Java 并发设计模式实战

系统讲解 Java 并发设计模式:生产者-消费者、工作窃取、不可变对象、读写分离、双重检查锁定、Copy-on-Write、Future 承诺与线程封闭等模式,包含适用场景、代码示例、JUC 落地与实战陷阱。

并发设计模式不是「炫技」,而是把并发难题收敛为可复用的解决方案:数据怎么共享、任务怎么协作、状态怎么保护。本文从经典的生产者-消费者讲起,覆盖工作窃取、不可变对象、读写分离、双重检查锁定等模式,每个模式都给出适用场景、代码骨架与 JUC 的落地方式,帮助你在真实业务中选对模式。

一、并发设计模式概览

1.1 为什么要并发模式

并发编程的三大难题:可见性、原子性、有序性。设计模式把这些难题包装成固定的协作形态,让并发代码「好写、好读、好维护」。

模式解决的核心问题典型应用
生产者-消费者解耦生产与消费速率消息队列、异步任务
工作窃取负载均衡 + 利用率ForkJoinPool
不可变对象天然线程安全值对象、缓存
读写分离读多写少的锁开销缓存、配置
双重检查锁定延迟初始化的安全写法单例
Copy-on-Write读多写极少的并发容器事件监听器列表
选型思路:
  共享数据 → 优先不可变 / 线程封闭
  协作分工 → 生产者-消费者 / 工作窃取
  读多写少 → 读写锁 / Copy-on-Write
  延迟创建 → 双重检查锁定 / Holder 类

一句话总结: 并发模式是把「共享、协作、保护」三大问题做成固定配方;先回答「数据形态」和「读写比例」,再选模式,就不会滥用锁。


二、生产者-消费者模式

2.1 核心思想

生产者与消费者之间用阻塞队列解耦:生产者只管放,消费者只管取,速率不一致时由队列缓冲。

// JUC 落地:用 BlockingQueue 免手写同步
BlockingQueue<Order> queue = new ArrayBlockingQueue<>(1000);

// 生产者:提交订单
Runnable producer = () -> {
    try { queue.put(new Order("A-1")); }
    catch (InterruptedException e) { Thread.currentThread().interrupt(); }
};

// 消费者:处理订单
Runnable consumer = () -> {
    try {
        Order order = queue.take();   // 队列空则阻塞等待
        process(order);
    } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
};

2.2 队列选型

队列特性适用
ArrayBlockingQueue有界数组,公平可配有界缓冲,防止堆积
LinkedBlockingQueue可选有界/无界链表吞吐高,默认无界慎用
SynchronousQueue不缓冲,直接交接严格一对一交接
PriorityBlockingQueue按优先级出队优先级任务
DelayQueue延迟到期才出队定时任务、重试
生产关键点:
  有界队列 + 拒绝策略,防止生产者无限堆积
  消费者失败要有重试/死信队列,避免丢失
  用 offer/poll 配合超时,而不是裸 put/take 死等

一句话总结: 生产者-消费者的核心是「阻塞队列 + 有界 + 超时」;用 ArrayBlockingQueue 做有界缓冲、用 offer 带超时,就能优雅应对峰值与背压。


三、工作窃取(Work Stealing)

3.1 原理

每个工作线程有自己的双端队列,任务不足时偷取其他线程队尾的任务,保持所有线程忙碌,最大化利用率。

ForkJoinPool 的工作窃取:
  1. 大任务 fork 成子任务,放入自己队列
  2. 线程空闲时从别人队列 steal 任务
  3. 任务合并用 join,形成递归分治

对比普通线程池:
  固定队列 → 空闲线程干等,任务堆积某队列
  工作窃取 → 空闲线程主动找活,利用率高
// ForkJoinPool 示例:并行求和
class SumTask extends RecursiveTask<Long> {
    private final long[] arr; private final int lo, hi;
    static final int THRESHOLD = 1000;

    @Override
    protected Long compute() {
        if (hi - lo <= THRESHOLD) {
            long sum = 0;
            for (int i = lo; i < hi; i++) sum += arr[i];
            return sum;
        }
        int mid = (lo + hi) >>> 1;
        SumTask left  = new SumTask(arr, lo, mid);
        SumTask right = new SumTask(arr, mid, hi);
        left.fork();            // 压入队列
        return right.compute() + left.join();  // 合并
    }
}

3.2 使用注意

陷阱:
  1. 任务太小 → fork 开销超过计算,设阈值
  2. 共享可变状态 → 子任务互相踩踏,要保持子任务独立
  3. join 顺序 → 先 fork 后 join,避免栈深爆炸

并行流 parallelStream() 底层就是 ForkJoinPool.commonPool

一句话总结: 工作窃取用「双端队列 + 空闲偷取」提升利用率,RecursiveTask 是递归分治的标准骨架;阈值设得太小、共享状态不隔离是两个最常见的坑。


四、不可变对象模式

4.1 为什么不可变最安全

不可变对象创建后状态不再变化,因此不存在可见性、原子性问题——任何线程看到的就是完整一致的状态。

// 不可变对象:final 字段 + 无 setter
public final class Point {
    private final int x;
    private final int y;
    public Point(int x, int y) { this.x = x; this.y = y; }
    public int x() { return x; }
    public int y() { return y; }
    // 更新返回新对象,而不是修改自身
    public Point move(int dx, int dy) {
        return new Point(x + dx, y + dy);
    }
}

4.2 不可变与性能的权衡

维度可变对象不可变对象
线程安全需同步天然安全
更新开销原地改,低复制新对象,高
缓存友好一般可安全共享
适用高频小范围修改配置、值对象、缓存 key
实践组合:
  更新频繁的少量字段 → 可变 + 锁
  低频但被广泛共享 → 不可变,无锁
  大量只读引用 → 不可变 + Copy-on-Write 容器

一句话总结: 不可变对象是「零成本线程安全」——final 字段 + 无 setter + 更新返回新对象;代价是频繁更新时的复制开销,适合共享多、修改少的数据。


五、读写分离(Read-Write Lock)

5.1 读多写少用读写锁

ReentrantReadWriteLock 让读读并发、写写互斥、读写互斥:读线程之间不阻塞,写线程独占。

ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
Map<String, Config> configs = new HashMap<>();

// 读路径:多个读线程可并发
Config get(String key) {
    rwLock.readLock().lock();
    try { return configs.get(key); }
    finally { rwLock.readLock().unlock(); }
}

// 写路径:独占,写时读也阻塞
void put(String key, Config c) {
    rwLock.writeLock().lock();
    try { configs.put(key, c); }
    finally { rwLock.writeLock().unlock(); }
}

5.2 读写锁 vs StampedLock

对比ReentrantReadWriteLockStampedLock
读并发支持支持,另有乐观读
乐观读无tryOptimisticRead 无锁读
重入支持不支持重入
适用通用读多写少纯读比例极高的热点
// StampedLock 乐观读:读时不加锁,validate 校验版本
StampedLock lock = new StampedLock();
long stamp = lock.tryOptimisticRead();
int v = value;                        // 乐观读
if (!lock.validate(stamp)) {          // 期间被写过 → 升级悲观读
    stamp = lock.readLock();
    try { v = value; }
    finally { lock.unlockRead(stamp); }
}

一句话总结: 读写分离用「读锁并发、写锁独占」服务读多写少;极致读场景用 StampedLock 乐观读,但要注意它不可重入、也更容易用错。


六、双重检查锁定(Double-Checked Locking)

6.1 为什么朴素写法是错的

延迟初始化单例的朴素写法在并发下有可见性问题——对象引用可见时,其内部字段可能尚未初始化完成。

// 错误示范:不安全,可能读到半初始化对象
private static Singleton instance;
public static Singleton getInstance() {
    if (instance == null) {            // 第一次检查
        synchronized (Singleton.class) {
            if (instance == null) {    // 第二次检查
                instance = new Singleton();
            }
        }
    }
    return instance;
}

6.2 正确姿势:volatile + DCL

// 正确:volatile 禁止指令重排,保证初始化可见
private static volatile Singleton instance;

public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        }
    }
    return instance;
}

6.3 更简单的替代:静态内部类

// 推荐:利用类加载机制保证线程安全,无需锁与 volatile
public class Singleton {
    private Singleton() {}
    private static class Holder {
        static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

一句话总结: DCL 必须配 volatile 才安全;能用静态内部类(Holder)就不用 DCL——同样懒加载、天然线程安全、代码更简单。


七、其他常用并发模式

7.1 Copy-on-Write 容器

读操作不加锁直接读副本,写操作复制新数组再替换引用。适合读极多、写极少的场景,如事件监听器列表。

// 事件监听器列表:遍历时读者拿到稳定快照
CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>();

listeners.add(listener);            // 写:复制数组,成本 O(n)
for (Listener l : listeners) {      // 读:无锁,拿的是快照
    l.onEvent(event);
}

7.2 Future 与承诺(Promise)

// 用 CompletableFuture 做"异步承诺"
CompletableFuture<Price> priceFuture =
        CompletableFuture.supplyAsync(() -> queryPrice("BTC"))
                .orTimeout(3, TimeUnit.SECONDS);   // 超时即失败

priceFuture.thenAccept(p -> System.out.println("价格=" + p));

7.3 线程封闭与 ThreadLocal

把可变状态封闭在线程内,避免共享。注意与虚拟线程时代的 ScopedValue 演进方向。

模式一句话适用
Copy-on-Write读写分离的极值版读极多写极少
Future/承诺异步结果占位符并行子任务编排
线程封闭状态不跨线程每线程独立缓冲
反应式背压速率控制流式处理

一句话总结: Copy-on-Write 用「快照读」换写开销;Future/承诺负责异步编排;线程封闭避免共享——模式是工具箱,按数据特征取用。


八、实战陷阱清单

陷阱现象对策
DCL 缺 volatile偶发半初始化加 volatile 或改 Holder
无界队列堆积内存上涨、消费滞后用有界队列 + 拒绝策略
fork 阈值太小性能反降压测设定阈值
读写锁写饥饿写线程长时间等待用公平锁或 StampedLock
Copy-on-Write 滥用写频繁复制爆炸仅用于读极多写极少
ThreadLocal 泄漏内存缓慢上涨用完 remove
子任务共享可变状态数据错乱子任务保持独立

九、总结

模式一句话JUC 落地
生产者-消费者阻塞队列解耦BlockingQueue
工作窃取空闲线程偷活ForkJoinPool
不可变对象零成本安全final + 复制
读写分离读并发写独占ReadWriteLock / StampedLock
双重检查锁定延迟初始化volatile + Holder
Copy-on-Write快照读CopyOnWriteArrayList

一句话记住:并发设计模式的核心是把「共享、协作、保护」做成固定配方。遇到并发问题先问三件事——数据改不改、读写比例、协作方式——再从这张表里挑答案,远比自己发明锁要稳。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. Java 序列化方案对比与性能
  2. OOM 排查与堆转储分析
  3. Arthas 线上诊断实战