Skip to content

并发编程进阶:锁、线程池与 JUC

难度:高级 | 预计时间:60 分钟 | 前置:多线程入门


📖 synchronized 不够用了

故事背景

你之前学了 synchronized,以为多线程问题已经解决了。直到有一天——你需要一个计数器每秒被 1000 个线程读取、10 个线程写入。synchronized 让所有读写互斥,读操作之间也在排队——吞吐量惨不忍睹。你又试了试 ExecutorService——线程池的拒绝策略、核心线程数、最大线程数、队列类型……参数多到让人崩溃。

这时你发现了 JUC(java.util.concurrent)——Doug Lea 大神写的并发工具包:ReentrantLock(比 synchronized 更灵活)、ReadWriteLock(读读不互斥)、ConcurrentHashMap(分段锁,超高并发)、ThreadPoolExecutor(精确控制线程池行为)、CountDownLatch(线程间协调)、CompletableFuture(异步编排)。这节课让你从"会用线程"升级到"能设计并发方案"。

💻 代码演示

JUC 核心工具一览——每个都是解决具体问题的利器:

java
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

public class JUCDemo {
    public static void main(String[] args) throws Exception {
        // 1. ReentrantLock — 比 synchronized 灵活,支持tryLock()和公平锁
        ReentrantLock lock = new ReentrantLock();
        lock.lock();
        try { /* 临界区 */ } finally { lock.unlock(); }

        // 2. ReadWriteLock — 读读不互斥,读写互斥(读多写少场景性能飙升)
        ReadWriteLock rwLock = new ReentrantReadWriteLock();
        rwLock.readLock().lock();   // 多个读线程可同时持有
        rwLock.writeLock().lock();  // 写锁独占

        // 3. ConcurrentHashMap — 高并发Map,不用整个表加锁
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        map.put("key", 1);
        map.computeIfAbsent("counter", k -> 0);  // 原子操作

        // 4. AtomicInteger — CAS无锁原子操作(比synchronized快10倍+)
        AtomicInteger counter = new AtomicInteger(0);
        counter.incrementAndGet();  // 原子自增,不用加锁

        // 5. ThreadPoolExecutor — 精确控制线程池
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
            4, 8, 60, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100),
            new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略
        );

        // 6. CountDownLatch — 等待N个线程全部完成
        CountDownLatch latch = new CountDownLatch(3);
        for (int i = 0; i < 3; i++) {
            pool.submit(() -> { doWork(); latch.countDown(); });
        }
        latch.await();  // 阻塞直到 countDown 到 0

        // 7. CompletableFuture — 异步编排(前端类比:Promise.all/race)
        CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Hello");
        CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "World");
        f1.thenCombine(f2, (a, b) -> a + " " + b)
          .thenAccept(System.out::println);  // Hello World
    }
    static void doWork() { /* ... */ }
}

运行输出:

JUC 提供了 synchronized 之外更精细的并发控制手段——读多写少用 ReadWriteLock、简单计数用 AtomicInteger、复杂编排用 CompletableFuture。

🎯 三个核心问题

这是什么?

JUC(java.util.concurrent)是 Java 5 引入的并发工具包,包含:原子类(AtomicInteger/Long/Reference,基于 CAS 无锁算法)、锁框架(ReentrantLock, ReadWriteLock, StampedLock)、并发集合(ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue)、线程池(ThreadPoolExecutor, ScheduledThreadPoolExecutor)、同步器(CountDownLatch, CyclicBarrier, Semaphore, Phaser)、异步工具(CompletableFuture, ForkJoinPool)。

为什么需要它?

synchronized 是"万能锁"但不够精细——读操作之间不需要互斥,但它强制互斥。ReadWriteLock 让读并发、写互斥——读多写少场景吞吐量提升数十倍。ThreadPoolExecutor 让你精确控制:核心线程数(常驻)、最大线程数(峰值)、队列容量(缓冲)、拒绝策略(兜底)——而不是 Executors.newFixedThreadPool 的一刀切。

如果没有它会怎样?

如果没有 JUC——每个并发场景你都得用 synchronized + wait/notify 手动实现:要自己写读写锁、要自己管理线程生命周期(不能复用线程导致频繁创建销毁开销巨大)、要自己实现 Future 和异步编排。JUC 把这些最佳实践固化为标准 API,大幅降低并发编程的出错率。

📝 原理讲解

JUC 选型速查表:

  • ReentrantLock:替代 synchronized。优势:可中断等锁(lockInterruptibly)、可超时(tryLock(1, SECONDS))、可公平/非公平。劣势:必须手动 unlock(放 finally 里)。

  • ReadWriteLock:读多写少(缓存场景)用这个。读锁共享、写锁独占。注意写锁降级(先获取写锁→再获取读锁→释放写锁)。

  • ConcurrentHashMap:替代 Hashtable 和 Collections.synchronizedMap。Java 8 用 CAS + synchronized 分段实现,并发度极高。

  • AtomicInteger:CAS 无锁原子操作——比 synchronized 快一个数量级。适合计数器、序列号等简单场景。

  • ThreadPoolExecutor 参数:corePoolSize(常驻)、maximumPoolSize(峰值)、keepAliveTime(空闲回收)、workQueue(缓冲队列)、rejectedExecutionHandler(拒绝策略:Abort/丢弃/调用者运行/丢弃最旧)。

  • CompletableFuture:前端类比 Promise。thenApply(map)、thenAccept(forEach)、thenCombine(all)、anyOf(race)、exceptionally(catch)。

  • CountDownLatch vs CyclicBarrier:Latch 一次性(等 N 件事完成),Barrier 可重用(等 N 个线程都到齐再一起走)。

🎨 生活类比

类比理解

synchronized 是厕所门锁——一次只能进一个人,管你是谁(读写都互斥)。 ReadWriteLock 是阅览室规则——多人可同时看书(读共享),但有人要写笔记时其他人必须出去(写独占)。 ThreadPoolExecutor 是出租车公司调度中心——核心司机(corePoolSize)一直在岗;忙时临时招兼职(maximumPoolSize - corePoolSize);排队乘客(workQueue)满了就启用拒绝策略(拒载/让乘客自己走/扔掉最老的订单)。 CountDownLatch 是发令枪——"等 3 个运动员都准备好"(countDown 到 0),然后开枪(await 返回)。 CompletableFuture 是外卖调度——"从 A 店取餐,从 B 店取饮料,两个都到了再打包送给你"(thenCombine)。

✏️ 动手练习

练习 1

用 ReadWriteLock 实现一个线程安全的缓存:多个线程读缓存,偶尔有线程更新缓存。对比 synchronized 版本的吞吐量差异。

<details> <summary>💡 查看提示</summary>

读操作用 readLock,写操作用 writeLock。模拟 100 个读线程 + 2 个写线程,统计两种方案的完成时间。

</details>

练习 2

用 ThreadPoolExecutor 创建线程池(core=2, max=4, 队列容量=3, CallerRunsPolicy 拒绝策略),提交 10 个任务,观察哪些任务在哪个线程执行。

<details> <summary>💡 查看提示</summary>

任务里打印 Thread.currentThread().getName()。当队列满且线程数达到 max 时,新任务会触发拒绝策略。

</details>

练习 3

用 CompletableFuture 模拟三个服务的并行调用:用户服务、订单服务、积分服务。三个服务都返回后,汇总结果返回。如果任一失败,返回降级数据。

<details> <summary>💡 查看提示</summary>

CompletableFuture.allOf(f1, f2, f3).thenApply(...) 或 f1.thenCombine(f2, ...).thenCombine(f3, ...)。降级用 .exceptionally(e -> fallbackData)。

</details>

✅ 自检站

<details> <summary><strong>ReadWriteLock 读锁和写锁的互斥关系是怎样的?为什么读多写少场景性能远超 synchronized?</strong></summary>

读读不互斥——多个线程可以同时持有读锁;读写互斥——有读锁时不能获取写锁,反之亦然;写写互斥。读多写少场景下,大部分时间多个线程可以并发读,只有偶尔写入才阻塞读——而 synchronized 连读操作都串行化。性能差距可达几十倍。

</details>

<details> <summary><strong>ThreadPoolExecutor 的核心线程数、最大线程数、队列容量如何配合工作?</strong></summary>

任务进来→核心线程处理(corePoolSize)→核心线程忙→任务进队列(workQueue)→队列满→创建新线程(不超过 maximumPoolSize)→线程也满→执行拒绝策略。技巧:IO 密集型任务核心线程数可以设大(2×CPU核数+1),CPU 密集型设小(CPU核数+1)。

</details>

<details> <summary><strong>AtomicInteger 基于 CAS,CAS 是什么?为什么 CAS 比 synchronized 快?</strong></summary>

CAS(Compare And Swap)是 CPU 级别的原子指令——"比较内存值是否为期望值,是则更新为新值"。整个过程是硬件保证的原子操作,无需操作系统介入。synchronized 涉及锁的获取/释放、线程阻塞/唤醒,需要操作系统内核态切换——开销大一个数量级。但 CAS 在高度竞争时会自旋重试,此时性能可能不如 synchronized。

</details>