并发设计模式
0. 总体
常见的8种简单并发设计模式中,可以分为:
- 避免共享变量
不可变模式、写时复制模式、线程本地存储模式 - 多线程版本if
保护性暂挂模式、放弃模式 - 简单分工
Thread-Per-Message模式、工作线程模式、生产者消费者模式

1. 不可变模式
1.1 定义
- 对象不可变,如String及基础包装类Integer/Long等。
- 如果需要修改,那么就创建一个新的不可变对象,而可变对象往往是修改自己的属性。比如String类的replace()方法修改的时候,并没有更改原字符串里面 value[]数组的内容,而是创建了一个新字符串。
1.2 享元模式
Boolean、Character、Byte、Short、Integer、Long、String等都用了享元模式。
比如Integer的valueOf(int i)方法,若是i的范围在[-128~127],那么会直接返回缓存的对应Integer实例对象。
基础类型包装类及String类等不适合作为锁的原因也是因为享元模式,看上去可能私有的锁,其实却是同一个实例。
2. 写时复制模式
需要修改的时候,不更新原有的实例数据,而是创建一个新的实例。比如刚刚提到的String类的replace()方法。
特点:
- 延时复制:只有在真正需要复制的时候才复制,而不是提前复制好
- 按需复制:可以只复制修改点。比如redis的bgsave过程,父进程只写时复制备份时修改的数据并重新指向新地址。
- 函数式编程应用广泛:函数式编程的基础是不可变性,所以修改操作需要写时复制模式来解决
如CopyOnWriteArraySet类、CopyOnWriteArrayList类。
3. 线程本地存储模式
Java最具有代表性的便是ThreadLocal类。
3.1 原理
3.1.1 底层实现
Thread实例持有ThreadLocalMap(其实是数组Entry[] table),key为ThreadLocal实例(数组索引计算规则为key.threadLocalHashCode & (table.length - 1)
),值为本地存储的泛型值。
class Thread { //内部持有ThreadLocalMap ThreadLocal.ThreadLocalMap threadLocals; } class ThreadLocal<T>{ public T get() { //首先获取线程持有的ThreadLocalMap ThreadLocalMap map = Thread.currentThread().threadLocals; //在ThreadLocalMap中查找变量 Entry e = map.getEntry(this); return e.value; } static class ThreadLocalMap{ //内部是数组而不是Map Entry[] table; //根据ThreadLocal查找Entry Entry getEntry(ThreadLocal key){ //省略查找逻辑 } } //Entry定义 static class Entry extends WeakReference<ThreadLocal>{ Object value; } }
3.1.2 设计原因
为何是Thread实例持有ThreadLocalMap,而不是ThreadLocalMap实例持有ThreadMap
- 若是ThreadLocalMap实例持有ThreadMap,那么由于ThreadLocal作为中间类明显生命周期更长,作为key的这些线程也不会消亡,会有内存泄漏问题。
- 每个Thread保存各自的键值对信息,也避免了有一个外部中间类掌握所有数据,数据封装更合理。
- Thread 持有 ThreadLocalMap,而且 ThreadLocalMap 里对 ThreadLocal 的引用还是弱引用(WeakReference),所以只要 Thread 对象可以被回收,那么 ThreadLocalMap 就能被回收。
弱引用: 当一个对象仅仅被weak reference(弱引用)指向, 而没有任何其他strong reference(强引用)指向的时候, 如果这时GC运行, 那么这个对象就会被回收,不论当前的内存空间是否足够,这个对象都会被回收。
3.1.3 内存泄漏原因
内存泄露:程序在申请内存后,无法释放已申请的内存空间。
-
原因
- 线程池场景
在使用线程池时,ThreadLocal设置的值可能会一直保留在线程中,而线程会被线程池重复使用。
ThreadLocal由于是弱引用,会在方法调用结束后被回收置为Null,Entry情况如{Null:val}。但是由于val是强引用,且线程不会结束,故不会被回收。
如果在线程执行结束后没有显示清理ThreadLocal(除了remove外,其实get/set也会清理Entry中key为Null的情况),线程池中的线程引用的 ThreadLocal的值可能一直存在,导致内存泄漏。 - 长时间持有
如果在某些场景下,ThreadLocal 中的值被长时间持有,并且没有及时清理,就会导致 ThreadLocal 对应的对象无法被垃圾回收。
- 线程池场景
-
处理措施
- 适时清理
在使用完 ThreadLocal 存储的值后,尽量调用 remove 方法将其清除。可以使用 ThreadLocal.remove() 或者使用 try-with-resources 语句来确保资源的及时释放。
或者try (ThreadLocal tl = new ThreadLocal()) { // 使用 ThreadLocal } ThreadLocal tl = new ThreadLocal(); try { // 使用 ThreadLocal } finally { tl.remove(); } - 使用withInitial时注意使用静态方法
如果定义 ThreadLocal 时使用了 withInitial 方法,确保提供的初始化方法是静态的,以避免持有对外部类实例的引用。ThreadLocal<Integer> tl = ThreadLocal.withInitial(Count::get); // 调用Count类的静态方法get获取值
- 适时清理
更多内存泄露细节可见ThreadLocal的内存泄露。
3. 2 例子
看一段代码,利用ThreadLocal给线程池中不同线程生为线程号。
class ThreadId { private static final AtomicLong atomicLong = new AtomicLong(1); private static final ThreadLocal<Long> threadLocals = ThreadLocal.withInitial(atomicLong::getAndIncrement); public static long get() { return threadLocals.get(); } } public class Test { public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(3); for (int i = 0; i < 10; i++) { executorService.submit(() -> { System.out.println(Thread.currentThread() + " " + ThreadId.get()); }); } executorService.shutdown(); } }
打印如下,无论怎么执行,每个线程之间的ThreadId.get()都是不同的,但是自身的值又是不变的。
Thread[pool-1-thread-2,5,main] 3 Thread[pool-1-thread-1,5,main] 1 Thread[pool-1-thread-2,5,main] 3 Thread[pool-1-thread-3,5,main] 2 Thread[pool-1-thread-2,5,main] 3 Thread[pool-1-thread-1,5,main] 1 Thread[pool-1-thread-2,5,main] 3 Thread[pool-1-thread-2,5,main] 3 Thread[pool-1-thread-3,5,main] 2 Thread[pool-1-thread-1,5,main] 1
4. 保护性暂挂模式(Guarded Suspension)
4.1 定义及原理
一个线程等待某个条件成立,而另一个线程负责改变这个条件。当条件改变后,等待的线程将收到通知并继续执行。
Guarded Suspension 模式的结构图:
- 一个对象
GuardedObject,内部有一个受保护的对象的成员变量 - 两个成员方法
get(Predicate p)和onChanged(T obj)方法。
经典实现如下。
class GuardedObject<T> { final Lock lock = new ReentrantLock(); final Condition done = lock.newCondition(); //受保护的对象 T obj; //获取受保护对象 T get(Predicate<T> p) { lock.lock(); try { //MESA管程推荐写法 while (!p.test(obj)) { done.await(); } } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } //返回非空的受保护对象 return obj; } //事件通知方法 void onChanged(T obj) { lock.lock(); try { this.obj = obj; done.signalAll(); } finally { lock.unlock(); } } }
使用例子如下。
public class Test { public static void main(String[] args) { GuardedObject<Integer> guardedObject = new GuardedObject<>(); Thread thread = new Thread(() -> { sleep(3); guardedObject.onChanged(1); }); thread.start(); System.out.println(guardedObject.get(t -> t!=null&&t > 0)); } @SneakyThrows public static void sleep(int i) { TimeUnit.SECONDS.sleep(i); } }
打印如下, 可以看到3秒后由于thread修改了受保护对象的值为1,guardedObject.get(t -> t!=null&&t > 0)
验证通过而返回。
22:31:00.336 await 22:31:01.345 await 22:31:02.358 await 1
其实juc包中的Future接口就是保护性暂停模式的应用。
4.2 异步等待消息的例子
服务A和服务B间通过MQ交互,其中服务A的发送消息和接收消息的线程归属不同的线程,那么发送消息怎样同步等待消息返回呢?(类似RPC接口等待消息返回。因为若是走tcp传递消息,由于tcp是异步的那么调用也是异步的,等待是实现了异步转同步)
首先由于是不同线程,为了能匹配上,需要利用一个唯一的标识。这里可以利用消息的唯一message id实现,作为Map的key,GuardedObject则作为Map的value保存。
需要针对原始的GuardedObject进行功能扩展支持以上功能。
这里选择编写一个内部静态代理类管理key和GuardedObject的映射关系。
class GuardedObject<T> { // 为了使用条件等待,创建锁 private final Lock lock = new ReentrantLock(); // 完成的条件变量 private final Condition done = lock.newCondition(); // 受保护的对象 private T object; // 禁止new创建,统一走GuardObjectProxy类创建 private GuardedObject() {} // 阻塞等待结果 // get可以传入超时参数给await使用,这里演示不添加了 public T get(Predicate<T> predicate) { lock.lock(); try { // MESA管程经典编程模式 while判断 while (!predicate.test(object)) { done.await(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } return object; } // 接收到数据时调用,某些框架里回调函数调用 public void onMessage(T object) { lock.lock(); try { this.object = object; done.signalAll(); } finally { lock.unlock(); } } // 管理GuardObject映射关系 public static class GuardObjectProxy<K, T> { // 单例 private static volatile GuardObjectProxy<?, ?> object = null; public Map<K, GuardedObject<T>> guardedObjectMap = new ConcurrentHashMap<>(); // 单例模式,使用一个guardedObjectMap private GuardObjectProxy() {} // 创建GuardObject public GuardedObject<T> createGuardObject(K k) { GuardedObject<T> guardedObject = new GuardedObject<>(); guardedObjectMap.put(k, guardedObject); return guardedObject; } // 查找GuardObject public GuardedObject<T> getGuardObject(K k) { return guardedObjectMap.get(k); } // 获取GuardObjectProxy实例 public static GuardObjectProxy<?, ?> getProxy() { if (Objects.isNull(object)) { synchronized (GuardObjectProxy.class) { if (Objects.isNull(object)) { object = new GuardObjectProxy<>(); } } } return object; } } } @Data @AllArgsConstructor class Message { private Integer id; private String content; @Override public String toString() { return "messageId:" + id + " content:" + content; } } public class Test { public static void main(String[] args) throws InterruptedException { // 获取代理类单例 GuardedObject.GuardObjectProxy<Integer, String> guardedObjectProxy = (GuardedObject.GuardObjectProxy<Integer, String>) GuardedObject.GuardObjectProxy.getProxy(); // 模拟线程发送线程池 ExecutorService sendExecutor = Executors.newFixedThreadPool(5); // 模拟线程接收线程池 ExecutorService receiveExecutor = Executors.newFixedThreadPool(5); // 模拟消息队列 BlockingQueue<Message> mq = new LinkedBlockingQueue<>(5); // 异步发送4条消息并等待结果 for (int i = 1; i < 5; i++) { Integer finalI = i; sendExecutor.submit(() -> { // 发送消息,message id为i try { Message message = new Message(finalI, "Message" + finalI); GuardedObject<String> guardedObject = guardedObjectProxy.createGuardObject(finalI); mq.put(message); System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " [send] " + message + " success"); System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " [send get wait] messageId:" + message.getId()); String receiveMessage = guardedObject.get(Objects::nonNull); System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " [send get end] " + receiveMessage + "\n"); } catch (InterruptedException e) { e.printStackTrace(); } }); } // 接收消息并修改对应状态 for (int i = 0; i < 5; i++) { receiveExecutor.submit(() -> { while (true) { try { Message message = mq.take(); System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " [receive] " + message); // 模拟消息处理时长,id为几就sleep几秒,观察接收阻塞情况 sleep(message.getId()); System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " [receive handle end] " + message); guardedObjectProxy.getGuardObject(message.getId()).onMessage("handle **" + message.getContent() + "**"); } catch (InterruptedException e) { e.printStackTrace(); } } }); } } @SneakyThrows public static void sleep(int time) { TimeUnit.SECONDS.sleep(time); } }
打印如下,可以看到pool-1的发送线程池的线程,按预期等待对应时长并接收到结果。
20:32:00.087 pool-1-thread-2 [send] messageId:2 content:Message2 success 20:32:00.087 pool-2-thread-4 [receive] messageId:3 content:Message3 20:32:00.087 pool-1-thread-4 [send] messageId:4 content:Message4 success 20:32:00.087 pool-2-thread-3 [receive] messageId:1 content:Message1 20:32:00.087 pool-1-thread-4 [send get wait] messageId:4 20:32:00.087 pool-1-thread-3 [send] messageId:3 content:Message3 success 20:32:00.087 pool-2-thread-2 [receive] messageId:2 content:Message2 20:32:00.087 pool-1-thread-1 [send] messageId:1 content:Message1 success 20:32:00.087 pool-1-thread-3 [send get wait] messageId:3 20:32:00.087 pool-2-thread-1 [receive] messageId:4 content:Message4 20:32:00.087 pool-1-thread-2 [send get wait] messageId:2 20:32:00.087 pool-1-thread-1 [send get wait] messageId:1 20:32:01.093 pool-2-thread-3 [receive handle end] messageId:1 content:Message1 20:32:01.093 pool-1-thread-1 [send get end] handle **Message1** 20:32:02.093 pool-2-thread-2 [receive handle end] messageId:2 content:Message2 20:32:02.093 pool-1-thread-2 [send get end] handle **Message2** 20:32:03.093 pool-2-thread-4 [receive handle end] messageId:3 content:Message3 20:32:03.093 pool-1-thread-3 [send get end] handle **Message3** 20:32:04.093 pool-2-thread-1 [receive handle end] messageId:4 content:Message4 20:32:04.093 pool-1-thread-4 [send get end] handle **Message4**
4.3 总结
Guarded Suspension模式本质上是一种等待唤醒机制的实现,只不过 Guarded Suspension 模式将其规范化。无需重头思考如何实现,也无需担心实现程序的可理解性问题,同时也能避免 Bug。
但 Guarded Suspension 模式在解决实际问题的时候,往往还是需要扩展的,扩展的方式有很多,如上文所示。
5. 放弃模式(Balking)
5.1 特点
- 如果条件不满足就放弃执行,不像保护性暂挂模式一样一直等到超时。
- 仅执行一次初始化方法
- 尽量减少加锁范围,只针对状态读取的操作进行加锁处理
- 若是仅有主线程和一个子线程,分别修改不同状态,可以仅使用volatile修饰状态变量,而不加锁
5.2 例子
5.2.1 锁实现
@Slf4j class Balking { // 数据修改状态 private boolean state; // Balking模式相关类初始化状态 private final AtomicBoolean initState = new AtomicBoolean(true); // 举例用的定期执行的线程池 private ScheduledExecutorService executor; public Balking() { check(); } // 随便写了个初始化方法,仅仅为了演示Balking模式只初始化一次 private void doInit() { if (initState.getAndSet(false)) { state = false; executor = Executors.newScheduledThreadPool(2); // 1秒后,每秒执行一次。这里使用两个线程进行竞争。 executor.scheduleWithFixedDelay(this::check, 1, 1, TimeUnit.SECONDS); executor.scheduleWithFixedDelay(this::check, 1, 1, TimeUnit.SECONDS); } } // 定期执行的方法 // 若是修改了数据就执行保存数据,否则直接放弃 private void check() { doInit(); // 利用锁保证可见性/原子性/顺序性,最小粒度加锁,判断完状态就取消锁 synchronized (this) { if (!state) { log.info("无事发生 "); return; } // 记得在临界区修改状态,不然存在竞态条件 state = false; } // 保存数据 saveData(); } // 修改状态 public void changeState(Boolean state) { // 利用锁保证可见性/原子性/顺序性 synchronized (this) { this.state = state; } } // 保存数据 private void saveData() { log.info(" 保存数据"); } // 修改数据 public void updateData() { log.info(" 修改数据"); changeState(true); } } public class Test { public static void main(String[] args) { Balking balking = new Balking(); // 等待3秒后修改数据 sleep(1); balking.updateData(); // 再等待2秒后修改数据 sleep(2); balking.updateData(); } @SneakyThrows public static void sleep(int time) { TimeUnit.SECONDS.sleep(time); } }
打印如下。
21:48:40.603 [main] INFO com.huaxiaogou.MDog.test.Balking - 无事发生 21:48:41.608 [pool-1-thread-2] INFO com.huaxiaogou.MDog.test.Balking - 无事发生 21:48:41.608 [pool-1-thread-1] INFO com.huaxiaogou.MDog.test.Balking - 无事发生 21:48:41.610 [main] INFO com.huaxiaogou.MDog.test.Balking - 修改数据 21:48:42.614 [pool-1-thread-2] INFO com.huaxiaogou.MDog.test.Balking - 保存数据 21:48:42.614 [pool-1-thread-1] INFO com.huaxiaogou.MDog.test.Balking - 无事发生 21:48:43.615 [main] INFO com.huaxiaogou.MDog.test.Balking - 修改数据 21:48:43.617 [pool-1-thread-2] INFO com.huaxiaogou.MDog.test.Balking - 无事发生 21:48:43.617 [pool-1-thread-1] INFO com.huaxiaogou.MDog.test.Balking - 保存数据 21:48:44.623 [pool-1-thread-2] INFO com.huaxiaogou.MDog.test.Balking - 无事发生
5.2.2 volatile实现
首先将之前代码加锁操作全部去掉。
其次没有了锁保证可见性,给state加上volatile。
// 数据修改状态 private volatile boolean state;
最后,将线程数调为1。由于本来就只有1个线程保存数据,也不会有什么问题。
executor = Executors.newSingleThreadScheduledExecutor(); // 1秒后,每秒执行一次 executor.scheduleWithFixedDelay(this::check, 1, 1, TimeUnit.SECONDS);
6. Thread-Per-Message模式
每个请求开一个线程单独处理,但是由于Java中的线程都是重量级线程,这个模式并不适用Java代码。
一般都是利用线程池重复使用线程。
JDK21支持虚拟线程了,其实就是协程。这个模式后续可能会有所发展。
7. Worker Thread模式
结构
对应Java中的应用其实就是常用的线程池,更多的线程池信息可以参考线程池详解。
7.2. 两阶段终止模式
-
设置一个标志位
终止一个线程,首先要把线程的状态从休眠状态转换到 RUNNABLE 状态。Thread 类提供的 interrupt() 方法可以实现。 -
响应终止指令
线程会在合适的时机检查这个标志位,如果发现符合终止条件,则自动退出 run() 方法。
更多的线程中断信息可以参考线程池子线程的终止Shutdown()、ShutdownNow()
8. 生产者-消费者模式
特点:
- 支持批量执行以提升性能
- 支持分阶段提交以提升性能
优点:
- 功能解耦
- 支持异步,能够平衡生产者和消费者的速度差异
8.1 支持批量执行以提升性能
比如有个服务,每来一个数据都需要insert一次,若是数据量较多频繁的insert会影响数据库性能。可以考虑丢入队列,消费者取出批量数据并通过批量插入接口一次调用插入。(仅举例,这样会有数据丢失风险)
8.2 支持分阶段提交以提升性能
将数据写入阻塞队列,子线程根据不同规则读取后进行不同处理。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?