并发与 JUC 深入
Java 高级 · 底层原理
高并发系统的稳定性,根本上取决于你对"锁怎么实现、线程怎么调度、任务怎么编排"的理解深度。本文不止讲结论,而是深入 AbstractQueuedSynchronizer(AQS)的 CLH 队列、线程池的 ctl 位运算、以及虚拟线程的载体调度,让你从"加个 synchronized"进阶到"精准控制并发"。
一、AQS:JUC 的心脏
ReentrantLock、Semaphore、CountDownLatch、ReadWriteLock 全都建立在一个名为 AbstractQueuedSynchronizer 的抽象类之上。它的设计极其精妙,用三个核心部件统一了所有同步器:
volatile int state:同步状态。锁的可重入次数、信号量的许可数、读写锁的读计数都编码在这里。- FIFO 等待队列(CLH 变种):一个双向链表,节点
Node封装"等待的线程 + 等待状态(CANCELLED/WAITING 等)"。 - 两套模板方法:
acquire/release(独占)与acquireShared/releaseShared(共享),子类只实现tryAcquire/tryRelease等钩子。
1.1 加锁失败的链路
以 ReentrantLock.lock() 为例,调用链是 lock → sync.acquire(1):
关键细节:acquire 先 tryAcquire(一次 CAS 抢锁),失败才进队列;进队后不是立即 park,而是再 tryAcquire 一次(避免不必要的挂起),仍失败才 LockSupport.park。这叫"自旋一小会儿再阻塞",兼顾了低竞争下的性能与高竞争下的 CPU 友好。
1.2 公平锁 vs 非公平锁
ReentrantLock 默认非公平:
// 非公平锁:上来先插队 CAS,抢不到才排队
final boolean nonfairTryAcquire(int acquires) {
if (compareAndSetState(0, acquires)) { // 无视排队,直接抢
setExclusiveOwnerThread(current);
return true;
}
// 已持有则重入
...
}
// 公平锁:hasQueuedPredecessors() 检查"前面是否有人排队",有则放弃插队
protected final boolean tryAcquire(int acquires) {
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { ... }
}非公平锁吞吐更高(减少上下文切换),但可能造成"饥饿";公平锁严格按队列顺序,避免饥饿却牺牲性能。ReentrantLock 的 state 还支持条件队列 Condition:await() 把线程从 AQS 队列移到 Condition 自己的单向等待队列,signal() 再把它移回 AQS 队列重新抢锁——这就是 await/signal 必须持有锁才能调用的原因。
TIP
为什么 AQS 队列是 CLH 的变种(双向而非单向)?因为取消(cancel)节点、判断前驱状态、唤醒后继都需要双向指针;且每个节点只关心"前驱是否释放了锁"这一信号,自旋/park 都基于前驱状态,这种局部性让 CAS 冲突降到最低。
二、线程池:ctl 位运算与拒绝策略
ThreadPoolExecutor 的精妙在于用一个原子整数 ctl 同时编码线程池运行状态(高 3 位)和工作线程数(低 29 位),一次 CAS 同时更新两者,避免状态与计数不一致。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// RUNNING(111) SHUTDOWN(000) STOP(001) TIDYING(010) TERMINATED(011)
private static int runStateOf(int c) { return c & ~CAPACITY; } // 取高3位
private static int workerCountOf(int c) { return c & CAPACITY; } // 取低29位2.1 任务提交流程
注意一个易错点:任务会先尝试填满核心线程,再入队列,队列满了才扩到最大线程数。所以如果你用无界队列,maximumPoolSize 永远用不上,线程数恒等于 corePoolSize。
2.2 四大拒绝策略
AbortPolicy(默认):抛RejectedExecutionException。CallerRunsPolicy:由提交任务的线程自己执行——一种温和的反压,让提交方慢下来。DiscardPolicy:静默丢弃,可能造成任务丢失,慎用。DiscardOldestPolicy:丢弃队列头最老任务,再尝试提交当前任务。
// IO 密集型:核心数放大;用有界队列 + 反压策略,杜绝无界堆积 OOM
ThreadPoolExecutor pool = new ThreadPoolExecutor(
8, 32, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), // 有界!
new ThreadFactoryBuilder().setNameFormat("biz-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()); // 满时调用方兜底2.3 异步编排:CompletableFuture 的依赖图
CompletableFuture 把回调地狱变成声明式编排,底层是基于 ForkJoinPool(或你指定的线程池)的有向无环依赖图。每个 Future 持有一个 Completion 链表,前一个完成时触发后继的 thenApply/thenCompose/thenCombine。
CompletableFuture<User> u = CompletableFuture.supplyAsync(() -> userSvc.get(id), pool);
CompletableFuture<List<Order>> o = u.thenComposeAsync(uv -> orderSvc.list(uv.getId()), pool); // 扁平化
CompletableFuture<Void> done = o.thenAcceptAsync(orders -> log.info("{} 单", orders.size()))
.exceptionally(ex -> { log.error("err", ex); return null; });
// thenCombine: 两个 Future 都完成才执行二元函数DANGER
CompletableFuture 默认用 ForkJoinPool.commonPool(),它是全局共享且线程数有限(=核数-1)的。若把大量 IO 阻塞任务丢进去,会拖垮所有依赖公共池的代码。IO 密集任务务必传独立线程池。另外 exceptionally 只捕获前一个阶段的异常,长链建议配合 handle 做统一收口。
三、虚拟线程(JDK 21+):载体线程调度
传统平台线程(Platform Thread)是 OS 线程的一对一映射,创建成本高(默认 1MB 栈),百万并发直接打爆内核。虚拟线程(Virtual Thread)是 JVM 管理的轻量实体:
- 一个**载体线程(Carrier Thread,即平台线程)**可挂载海量虚拟线程。
- 虚拟线程遇到阻塞 IO /
LockSupport.park时,JVM 自动**卸载(unmount)它,把载体线程让给别的虚拟线程;IO 就绪后再挂载(mount)**回来。 - 因为"阻塞"不再阻塞载体线程,所以同步写法也能获得异步吞吐,且调试栈是可读的(不像回调嵌套)。
// 每个任务一个虚拟线程,海量并发也不爆内存
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000_000; i++) {
executor.submit(() -> blockingCall()); // 阻塞时自动卸载,载体线程去干别的
}
}WARNING
虚拟线程不适合 synchronized 包裹的密集计算或长持锁——synchronized 阻塞时无法卸载(会 pin 住载体线程)。应用 ReentrantLock 替代,或在虚拟线程里避免把阻塞操作放进 synchronized。CPU 密集计算也不适合虚拟线程(它没有减少计算量,只是省了阻塞时的栈成本)。
四、生产落地清单
- 按业务隔离线程池(下单/查询/通知分属不同池),避免相互挤占;用 Micrometer 暴露活跃线程数、队列长度、拒绝次数并告警。
- 锁要小:只在临界区持锁,远程调用/IO 放到锁外;
volatile不替代锁(i++仍需原子类或锁)。 - 善用原子类:
AtomicReference/LongAdder(高并发计数比AtomicLong分段,减少 CAS 争用)。 - IO 密集且阻塞多的场景优先评估虚拟线程,简化代码同时提升吞吐。
📚 官方文档与参考资料
- Java 并发包(JUC)API 文档:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/package-summary.html
- OpenJDK 虚拟线程(JEP 444):https://openjdk.org/jeps/444
- AQS 设计论文(CLH 队列):https://gee.cs.oswego.edu/dl/papers/aqs.pdf
✏️ 练习与自测
- 读
ReentrantLock.NonfairSync源码,画出lock()从tryAcquire到park的完整路径,说明非公平为何吞吐更高。 - 给定
core=2, max=4, queue=2,依次提交 10 个耗时任务,推演线程创建、入队、扩容、拒绝的每一个时刻状态。 - 用
CompletableFuture编排"查用户 → 并发查订单和地址 → 汇总",并指定独立线程池避免污染公共池。 - 对比虚拟线程与平台线程在 10 万并发阻塞调用下的资源占用,指出
synchronized为何会 pin 住载体线程。
本节小结
AQS 用 "state + CLH 双向队列 + LockSupport" 统一了所有同步器,公平/非公平只差"插队与否";线程池的精髓在 "ctl 位运算编码状态与计数 + 队列缓冲 + 拒绝策略反压";异步编排靠 CompletableFuture 依赖图,未来属于虚拟线程的"阻塞即卸载"。核心心法:锁要小、池要隔离、异步要有界、虚拟线程避 synchronized。