核心设计原则
在深入具体的模式之前,理解这些底层的设计原则至关重要,它们是指导我们如何思考并发问题的“指导思想”。

不可变性原则
- 核心思想:一个对象在被创建之后,其状态(内部数据)就不能再被修改,所有对“修改”的操作,都会返回一个全新的对象。
- 为什么重要:
- 线程安全:由于状态不可变,天生就不存在并发修改的问题,多个线程可以同时读取一个不可变对象,而无需任何同步措施。
- 简化设计:无需考虑锁、同步等复杂机制,极大地降低了程序的复杂性和出错的可能性。
- 实现方式:
- 将类声明为
final,防止被继承。 - 所有字段声明为
private final。 - 不提供任何修改字段的方法(如
set方法)。 - 确保在构造函数中完成所有初始化,并且不逸出
this引用(防止在对象完全构造好之前被其他线程看到)。
- 将类声明为
- Java 中的例子:
String类:所有修改操作(如substring,replace)都返回新的String实例。- 包装类(
Integer,Long等)。 - 枚举类型。
最小化锁的范围原则
- 核心思想:只对必须同步的代码块加锁,并且这个代码块要尽可能小,避免对整个方法加锁,除非整个方法体的所有操作都必须是原子的。
- 为什么重要:
- 提高并发性:锁的粒度越小,同一时间能获取锁的线程就越多,程序的并发吞吐量越高。
- 减少锁竞争:大范围的锁会导致线程长时间等待,增加上下文切换的开销,降低性能。
- 反面示例:
// 错误:对整个方法加锁,即使方法中只有一行代码需要同步 public synchronized void badMethod() { // 很多不涉及共享状态的代码... sharedList.add(item); // 只有这里需要同步 // 更多不涉及共享状态的代码... } - 正确示例:
// 正确:只对需要同步的代码块加锁 public void goodMethod() { // 很多不涉及共享状态的代码... synchronized (lock) { // 使用一个专门的对象作为锁,而不是 this 或方法参数 sharedList.add(item); } // 更多不涉及共享状态的代码... }
避免锁住可变对象原则
-
核心思想:永远不要用一个外部可变对象作为同步锁,如果锁对象本身可以被其他代码修改,那么它的
hashCode()和equals()方法可能会改变,导致synchronized块失效。 -
为什么重要:如果锁对象被修改,可能会导致同一个线程无法再次进入它之前加锁的代码块,或者更糟,导致两个不同的对象被误认为是同一个锁,从而引发死锁或数据竞争。
-
正确做法:
-
为每个需要同步的类或代码块创建一个私有的、不可变的
final锁对象。
(图片来源网络,侵删)public class MyClass { // 私有、不可变的锁对象 private final Object lock = new Object(); public void doSomething() { synchronized (lock) { // ... } } }
-
线程封闭原则
- 核心思想:将数据限制在单个线程内,只有创建该数据的线程才能访问它,这样一来,就不存在多线程共享数据的问题,自然也就无需同步。
- 为什么重要:这是实现线程安全最简单、最有效的方法之一,它将并发问题从“多线程如何协作”简化为“单线程如何处理”。
- 实现方式:
- 方法封闭:数据作为局部变量存在,只在方法内部使用。
- 线程局部:使用
ThreadLocal变量,为每个线程都创建一个独立的副本。
- Java 中的例子:
ThreadLocal:经典的实现,例如在SimpleDateFormat的使用中,为了避免其非线程安全性,可以为每个线程创建一个ThreadLocal<SimpleDateFormat>实例。
优先使用并发工具而非 synchronized
- 核心思想:在 Java 5 之后,
java.util.concurrent包提供了大量更高级、更强大、性能更好的并发工具,在大多数情况下,应该优先使用它们,而不是直接使用底层的synchronized关键字。 - 为什么重要:
- 功能更强大:提供了更丰富的同步语义,如读写锁、信号量、倒计时门闩等。
- 性能更好:很多工具(如
ReentrantLock)支持公平锁、非公平锁,并且能提供更好的吞吐量。 - 使用更灵活:可以响应中断、尝试获取锁(
tryLock)、设置超时等。
- 常用工具:
java.util.concurrent.locks包:ReentrantLock,ReentrantReadWriteLockjava.util.concurrent.atomic包:AtomicInteger,AtomicReference等,基于 CAS 机制,性能极高。java.util.concurrent包:CountDownLatch,CyclicBarrier,Semaphore,ConcurrentHashMap,BlockingQueue等。
核心设计模式
设计模式是解决特定问题的成熟方案,在并发编程领域,有一些经典的模式。
不可变对象模式
- 模式描述:如前所述,创建一个一旦创建就不可修改的对象,所有“修改”操作都返回一个新对象。
- 应用场景:
- 需要在多线程间安全地共享对象。
- 作为
Map的键或Set的元素,因为它们的哈希值不会变,非常高效。 - 缓存、配置信息等。
- Java 例子:
java.lang.Stringjava.lang.Integer及其所有包装类。- 所有 Java 枚举类型。
线程局部存储模式
- 模式描述:为使用它的每个线程都维护一个独立的变量副本,每个线程只能看到和修改自己的副本,互不干扰。
- 应用场景:
- 有状态的工具类,如
SimpleDateFormat、Random。 - 保存用户会话信息、事务上下文等。
- 有状态的工具类,如
- Java 例子:
java.lang.ThreadLocal是该模式的直接实现。
主动对象模式
- 模式描述:将方法调用和方法执行分离,当一个线程调用一个对象的方法时,它并不是立即执行该方法,而是将这个调用请求放入一个队列中,然后立即返回,该对象内部有一个专用的线程,它会从队列中取出请求并执行,这使得调用方线程和方法执行方线程可以完全解耦。
- 应用场景:
- 耗时的 I/O 或计算操作,不希望阻塞调用方线程。
- 需要精细控制任务执行顺序和优先级的场景。
- Java 例子:
java.util.concurrent.ExecutorService和Future模式是该思想的一种体现,你提交一个任务(Runnable/Callable),得到一个Future对象,可以稍后获取结果,而提交线程不会被阻塞。CompletableFuture是更现代、更强大的实现,支持函数式组合和异步编排。
线程池模式
- 模式描述:创建一组可重用的线程(线程池),而不是为每个任务都创建和销毁一个新线程,任务被提交到线程池,由池中的空闲线程来执行。
- 应用场景:
- 需要处理大量短期异步任务的场景(如 Web 服务器请求处理)。
- 避免频繁创建和销毁线程带来的性能开销。
- 控制并发线程数量,防止系统资源耗尽。
- Java 例子:
java.util.concurrent.ExecutorService是线程池的顶层接口。java.util.concurrent.Executors提供了创建各种预配置线程池的工厂方法,如newFixedThreadPool,newCachedThreadPool,newScheduledThreadPool。
Balking 模式(犹豫/观望模式)
-
模式描述:当某个操作因为对象的状态而不适合执行时,就放弃执行,直接返回,简单说就是“我只在合适的时候干活,不合适就算了”。
-
应用场景:
- 某个操作只需要执行一次,比如初始化。
- 文件保存操作,当文件内容没有发生变化时,无需重复保存。
-
结构:
(图片来源网络,侵删)- 一个条件判断(
if语句)。 - 一个
synchronized块,确保条件判断和后续操作的原子性。
- 一个条件判断(
-
示例代码:
public class SaverThread extends Thread { private final Data data; public SaverThread(Data data) { this.data = data; } public void run() { try { while (true) { // 检查是否有变化 if (data.isChanged()) { // 保存操作 data.save(); data.setChanged(false); // 保存后重置状态 } Thread.sleep(1000); } } catch (InterruptedException e) { // ... } } } public class Data { private boolean changed = false; // ... 其他字段 // synchronized 确保检查和修改的原子性 public synchronized void change() { this.changed = true; } public synchronized boolean isChanged() { return this.changed; } public synchronized void save() { // ... 实际保存逻辑 } }
Read-Write Lock 模式(读写锁模式)
- 模式描述:一种特殊的锁,它区分读操作和写操作,允许多个读线程同时访问共享资源,但写操作是独占的(写线程会阻塞所有其他读线程和写线程)。
- 应用场景:
- 读多写少的场景,一个配置信息被频繁读取,但很少被修改。
- 能极大地提高并发读的性能。
- Java 例子:
java.util.concurrent.locks.ReentrantReadWriteLock。
Guarded Suspension 模式(守护悬挂模式)
- 模式描述:当一个线程在某个条件未满足时,它会挂起(等待),直到其他线程使该条件满足后,它才会被唤醒并继续执行。
- 应用场景:
- 生产者-消费者模型,消费者线程在没有数据时会等待,生产者线程在数据队列满时会等待。
- 任何需要等待某个事件发生的场景。
- Java 例子:
java.lang.Object.wait()/notify()/notifyAll()是其基础实现。java.util.concurrent.BlockingQueue(如LinkedBlockingQueue)是对该模式的完美封装,使用起来非常简单。
高级模式与思想
Copy-on-Write 模式(写时复制)
- 模式描述:当需要修改一个集合时,不直接在原集合上进行修改,而是创建该集合的一个副本,在副本上进行修改,最后用这个新副本替换掉旧引用,读操作则无需加锁,直接访问集合。
- 应用场景:
- 读多写少的集合操作场景。
- 遍历操作远多于修改操作的场景,因为它可以避免遍历时加锁。
- 缺点:写操作的成本较高,因为需要复制整个集合。
- Java 例子:
java.util.concurrent.CopyOnWriteArrayListjava.util.concurrent.CopyOnWriteArraySet
Future 模式
- 模式描述:一个对象代表一个异步计算的结果,你可以启动一个计算,然后立即去做其他事情,之后可以通过
Future对象来获取计算的结果(如果还没算好,获取操作会阻塞)。 - 应用场景:
- 执行一个耗时的计算,但不想阻塞主流程。
- 并行执行多个独立任务,最后汇总结果。
- Java 例子:
java.util.concurrent.Future接口。java.util.concurrent.CompletableFuture是其增强版,支持链式调用、组合和异常处理,功能非常强大。
| 原则/模式 | 核心思想 | 适用场景 | Java 示例 |
|---|---|---|---|
| 不可变性原则 | 对象创建后状态不变 | 多线程共享数据、Map 键 | String, Integer |
| 最小化锁范围 | 只同步必要的代码块 | 提高并发性能 | synchronized (lock) { ... } |
| 避免锁住可变对象 | 使用私有、不可变的锁对象 | 防止锁失效 | private final Object lock = new Object(); |
| 线程封闭原则 | 数据只在一个线程内可见 | 简化并发设计 | ThreadLocal |
| 优先使用并发工具 | 使用高级工具而非 synchronized |
获得更好性能和功能 | ReentrantLock, ConcurrentHashMap |
| 线程局部存储模式 | 每个线程有独立数据副本 | 有状态工具类、会话管理 | ThreadLocal |
| 主动对象模式 | 调用与执行分离 | 异步任务、解耦 | ExecutorService, CompletableFuture |
| 线程池模式 | 重用线程,避免频繁创建/销毁 | 大量短期异步任务 | ThreadPoolExecutor |
| Balking 模式 | 状态不满足则放弃执行 | 单次初始化、条件性保存 | if (isReady()) { doWork(); } |
| 读写锁模式 | 读共享,写独占 | 读多写少场景 | ReentrantReadWriteLock |
| Guarded Suspension | 条件不满足则等待 | 生产者-消费者 | BlockingQueue, wait/notify |
| Copy-on-Write | 写时复制,读不加锁 | 读远多于写 | CopyOnWriteArrayList |
| Future 模式 | 异步获取计算结果 | 非阻塞调用耗时任务 | Future, CompletableFuture |
掌握这些原则和模式,并能在实际项目中灵活运用,是衡量一个 Java 开发者并发编程能力的重要标准。没有银弹,选择哪种方案需要根据具体的业务场景、性能要求和代码复杂度进行权衡。
