Java 并发编程(二):AQS 框架 + 线程池核心机制
线程池参数没有万能公式——CPU密集、IO密集、混合型,每种场景的决策逻辑完全不同。7 个参数是联动的方程组:改了 corePoolSize,maxPoolSize 的边界条件就变了;改了队列容量,拒绝策略的意义就不同了。我见过太多人背"CPU核数+1"然后被面试官一句"为什么"问倒。这篇文章给你 5 条推导式原则——每条都告诉你"为什么是这个数",而不是"背这个数"。
阅读约 15 分钟 | 系列第 7/17 篇
一、AQS:JUC 并发包的骨架
AQS(AbstractQueuedSynchronizer)是 java.util.concurrent(JUC)包的核心框架。ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 均基于 AQS 构建——AQS 提供锁的排队与调度机制,子类仅需实现尝试获取/释放锁的逻辑。
两大核心数据结构
(1)state(volatile int)
表示同步状态,在不同子类中含义不同:
| 子类 | state 含义 |
|---|---|
| ReentrantLock | 0 = 未占用,1 = 独占,>1 = 重入次数 |
| Semaphore | 剩余许可数量 |
| CountDownLatch | 尚未完成的计数 |
| ReentrantReadWriteLock | 高 16 位 = 读锁持有次数,低 16 位 = 写锁重入次数 |
(2)CLH 队列变体
CLH 队列是一个双向链表,每个节点代表一个等待线程。当线程获取锁失败时,AQS 将其封装为 Node 节点加入队尾,并通过 LockSupport.park() 阻塞该线程。前驱节点释放锁时唤醒后继节点,保证 FIFO 公平性。
设计要点:AQS 的 CLH 变体采用阻塞而非自旋——标准 CLH 队列在线程获取锁失败后持续自旋等待,适合临界区极短的场景;Java 版本改用 park/unpark,避免 CPU 空转,更适合临界区较长的业务代码。
获取锁流程
tryAcquire(子类实现)
→ 成功:获取锁,线程继续执行
→ 失败:
① addWaiter:创建独占模式 Node,CAS 追加到 CLH 队尾
② acquireQueued:自旋
前驱节点是 head 且 tryAcquire 成功 → 当前节点出队成为新 head
否则 → shouldParkAfterFailedAcquire 判断是否需要 park() 挂起
shouldParkAfterFailedAcquire 的核心逻辑:检查前驱节点的 waitStatus——
SIGNAL(前驱已承诺唤醒当前线程)→ 当前线程可以安全 parkCANCELLED→ 跳过已取消的前驱节点- 其他状态 → CAS 将前驱节点的 waitStatus 设为 SIGNAL
释放锁流程
tryRelease(子类实现)
→ 成功:unparkSuccessor 唤醒 CLH 队列中的后继节点
→ 后继节点被唤醒 → 回到 acquireQueued 自旋 → tryAcquire 尝试获取锁 → 成功则出队
"AQS 的架构可以概括为 state(同步状态)+ CLH 队列(排队机制)+ 模板方法(tryAcquire/tryRelease 由子类实现)。state 是 volatile 变量,通过 CAS 无锁修改,保证竞争场景下的性能;CLH 队列通过 park/unpark 实现阻塞而非自旋等待。三者组合是模板方法模式的典型应用。"
Condition:精准线程唤醒机制
synchronized 配合 wait()/notify() 只能实现单向通知——notify() 随机唤醒一个等待线程,notifyAll() 唤醒全部,无法定向唤醒特定条件的线程。Condition 通过 AQS 的条件队列实现精准唤醒:
AQS 内部维护两种队列:
同步队列(CLH Queue):等待获取锁的线程
条件队列(Condition Queue):执行 await() 后释放锁、等待特定条件的线程
await() 执行流程:
① 将当前线程包装为 Node 节点,从同步队列转移到条件队列末尾
② 完全释放锁(state 清零),唤醒同步队列中的后继节点
③ park() 挂起,等待 signal() 唤醒
signal() 执行流程:
① 将条件队列头节点从条件队列移到同步队列末尾
② 该节点在同步队列中排队,重新竞争锁
③ 获取锁后从 await() 调用处返回,继续执行
| synchronized + wait/notify | Lock + Condition | |
|---|---|---|
| 等待队列 | 每个监视器对象只有一个隐式等待集合 | 一个 Lock 可创建多个 Condition |
| 唤醒精度 | notify() 随机唤醒一个,notifyAll() 唤醒全部 | signal() 唤醒指定 Condition 队列中的一个线程 |
| 典型场景 | 简单生产者-消费者 | 有界缓冲区的精准通知(缓冲区满→等待 notFull;缓冲区空→等待 notEmpty) |
"Condition 的核心价值是按条件分组等待。以有界阻塞队列为例:生产者线程在 notFull 条件上等待,消费者线程在 notEmpty 条件上等待。生产者只唤醒消费者、消费者只唤醒生产者——避免无效唤醒导致的上下文切换损耗。这是 synchronized + wait/notify 无法实现的精度。"
二、线程池:七个核心参数与执行原理
七个核心参数
new ThreadPoolExecutor(
corePoolSize, // 核心线程数(常驻线程)
maximumPoolSize, // 最大线程数(峰值上限)
keepAliveTime, // 非核心线程空闲存活时间
unit, // keepAliveTime 的时间单位
workQueue, // 阻塞队列(任务缓冲区)
threadFactory, // 线程工厂(自定义线程命名与异常处理)
handler // 拒绝策略(队列满、线程满时的处理方式)
);
完整执行流程
任务提交
→ 当前工作线程数 < corePoolSize?
是 → 创建核心线程执行新任务(即使存在空闲核心线程也会创建)
否 → 尝试将任务放入阻塞队列(workQueue.offer)
入队成功 → 任务在队列中等待执行
入队失败(队列已满)→ 当前线程数 < maximumPoolSize?
是 → 创建非核心线程执行新任务
否 → 执行拒绝策略 RejectedExecutionHandler
关键设计要点:
- 核心线程默认永不销毁,除非显式调用
allowCoreThreadTimeOut(true) - 非核心线程空闲时间超过 keepAliveTime 后自动销毁
- 优先入队列而非立即创建新线程的设计意图:降低线程创建销毁开销,用队列缓冲突发流量
四种阻塞队列选型
| 队列 | 容量 | 特点 | 适用场景 |
|---|---|---|---|
| SynchronousQueue | 0,不存储元素 | 每个插入操作必须等待对应的移除操作,提交后必须有线程立即处理 | CPU 密集型小任务,CachedThreadPool 采用此队列 |
| LinkedBlockingQueue | 无界(Integer.MAX_VALUE) | 任务可无限入队,maximumPoolSize 参数失效 | FixedThreadPool 采用此队列,适合平稳流量 |
| ArrayBlockingQueue | 有界,需显式指定容量 | 队列满后触发创建非核心线程,天然背压机制 | 生产环境推荐——防止无界队列耗尽内存 |
| DelayedWorkQueue | 无界延迟队列 | 按任务延迟时间排序出队 | ScheduledThreadPool 采用此队列 |
四种拒绝策略
| 策略 | 行为 | 适用场景 |
|---|---|---|
| AbortPolicy(默认) | 抛出 RejectedExecutionException | 关键业务任务——宁可报错通知上游,不可静默丢弃 |
| CallerRunsPolicy | 由提交任务的线程自行执行,形成自然限流 | 能接受调用者线程偶尔阻塞执行的场景 |
| DiscardPolicy | 静默丢弃新任务 | 日志上报、埋点统计等非关键任务 |
| DiscardOldestPolicy | 丢弃队首最早入队的任务,重试提交新任务 | 优先处理最新任务的场景 |
生产推荐方案:AbortPolicy + 异常捕获后的监控告警,或自定义拒绝策略(重试 N 次 → 写入告警日志 → 兜底丢弃)。
参数设置公式
| 场景 | 公式 | 原理 |
|---|---|---|
| CPU 密集型 | corePoolSize = CPU 核数 + 1 | 避免频繁上下文切换,+1 作为备用 |
| IO 密集型 | corePoolSize = CPU 核数 × (1 + IO耗时/CPU耗时)(此为经验公式,理论最优值受限于 IO 设备的并发能力,需实测验证),粗略值 核数 × 2 | IO 等待期间 CPU 空闲,可容纳更多线程 |
| 混合型 | 动态线程池(配置中心调整核心参数) | 根据运行时指标弹性伸缩 |
三个常见陷阱
- Executors 工厂方法的隐患:
newFixedThreadPool使用无界 LinkedBlockingQueue,任务堆积最终 OOM;newCachedThreadPool允许无限创建线程,同步提交密集任务时线程数爆炸。生产环境必须直接使用new ThreadPoolExecutor(...)显式指定所有参数。 - submit() 吞异常:
submit()返回 Future 对象,若不调用get()方法,任务内部抛出的异常不会传播,静默丢失。轻量任务建议使用execute()替代,或对 Future.get() 包裹 try-catch。 - 线程池未隔离:不同业务共用同一线程池,慢任务可能耗尽所有线程,导致快任务排队超时。核心业务与外围业务应使用独立线程池。
线程池运行时监控
生产环境中线程池参数不宜硬编码——业务峰谷变化时,固定参数可能导致资源浪费或请求堆积:
executor.getPoolSize(); // 当前线程数(含核心与非核心)
executor.getActiveCount(); // 活跃线程数(正在执行任务的线程)
executor.getQueue().size(); // 队列中等待执行的任务数
executor.getCompletedTaskCount(); // 已完成任务总数
executor.getLargestPoolSize(); // 历史峰值线程数
监控告警规则:活跃线程数/核心线程数 > 0.8 且队列容量持续增长 → 触发扩容通知或限流降级。
三、异步任务编排:CompletableFuture
CompletableFuture 是 Java 8 的异步编排利器,解决"回调地狱"——多个异步任务有依赖关系时不再需要嵌套回调。
核心 API 速查
| 方法 | 作用 | 示例 |
|---|---|---|
supplyAsync(fn, pool) | 提交有返回值的异步任务 | CompletableFuture.supplyAsync(() -> queryPrice(), bizPool) |
thenApply(fn) | 拿上一个结果做同步转换(类似 map) | .thenApply(price -> price * 1.1) |
thenCompose(fn) | 拿上一个结果做异步任务(类似 flatMap) | .thenCompose(order -> saveOrderAsync(order)) |
thenCombine(cf, fn) | 等两个独立任务都完成,合并结果 | cf1.thenCombine(cf2, (r1, r2) -> r1 + r2) |
thenAccept(consumer) | 消费结果,无返回值 | .thenAccept(result -> log.info("done: {}", result)) |
exceptionally(fn) | 异常恢复,返回默认值 | .exceptionally(ex -> fallbackValue) |
allOf(cfs) | 等所有任务完成 | CompletableFuture.allOf(cf1, cf2, cf3).join() |
anyOf(cfs) | 任一任务完成即返回 | CompletableFuture.anyOf(cf1, cf2) |
实战:支付回调异步处理链
CompletableFuture.supplyAsync(() -> queryOrder(orderId), bizPool) // ① 查订单
.thenApply(order -> updateOrderStatus(order, "PAID")) // ② 同步更新状态
.thenCompose(order -> CompletableFuture.supplyAsync( // ③ 异步发通知
() -> sendNotification(order), bizPool))
.exceptionally(ex -> { // ④ 异常兜底
log.error("支付处理失败", ex);
return fallbackResponse;
});
四个常见坑
- 默认线程池是 ForkJoinPool.commonPool() ——被所有 CF 和 parallelStream 共用,一个慢任务卡全局。生产必须指定自定义线程池
- get() 在业务线程调用会阻塞——失去异步意义。用 thenAccept/thenApply 做回调
- thenApply vs thenCompose 混用——异步任务嵌套用 thenCompose,否则返回
CompletableFuture<CompletableFuture<T>>双重包装 - 异常默认被吞——必须加 exceptionally() 或 handle() 兜底
一句话:CompletableFuture 让异步代码从"回调金字塔"变成"链式流水线"。关键三原则:生产必须指定线程池 + 异常必须处理 + 异步任务链用 thenCompose 避免双重包装。
四、进阶:Disruptor 无锁环形队列
BlockingQueue 的核心性能瓶颈是 ReentrantLock——生产者和消费者竞争同一把锁。Disruptor(LMAX 交易所开源)把无锁编程做到极致,吞吐量可达 BlockingQueue 的 5-10 倍。
三大核心机制
1. RingBuffer(环形缓冲区) :预分配一个固定大小的数组,不扩容、不 GC。生产者往槽位写数据,消费者从槽位读数据,读完复用——如同环形跑道。
2. Sequence(序列号)+ CAS:每个生产者和消费者维护自己的序号(数组下标),通过 CAS 无锁递增抢占下一个可用槽位。关键——没有锁,只有 CAS + 内存屏障。
3. 缓存行填充(避伪共享) :CPU 加载数据以缓存行(64 字节)为单位。如果两个线程各自修改的变量落在同一缓存行 → 互相失效对方缓存 → 性能下降(伪共享)。Disruptor 在 Sequence 前后各填充 7 个 long(56 字节),加上 Sequence 本身 8 字节正好 64 字节,确保每个线程的 Sequence 独占一行。
核心代码
Disruptor<OrderEvent> disruptor = new Disruptor<>(
OrderEvent::new, // 事件工厂
1024, // RingBuffer 大小(必须是 2 的幂)
DaemonThreadFactory.INSTANCE,
ProducerType.SINGLE,
new BlockingWaitStrategy()
);
disruptor.handleEventsWith((event, seq, endOfBatch) -> processOrder(event));
disruptor.start();
// 生产者发布事件
long seq = ringBuffer.next(); // CAS 获取下一个槽位号
OrderEvent event = ringBuffer.get(seq);
event.setOrder(order);
ringBuffer.publish(seq); // 发布,消费者可见
BlockingQueue vs Disruptor
| BlockingQueue | Disruptor | |
|---|---|---|
| 数据结构 | 链表/数组,节点动态分配 | RingBuffer,预分配数组 |
| 并发控制 | ReentrantLock(有锁) | CAS + Volatile(无锁) |
| GC 压力 | 节点创建/销毁产生垃圾 | 预分配,零 GC |
| 吞吐量 | 十万级 QPS | 百万级 QPS |
| 生产选型 | 99% 的场景够用 | 金融高频交易/低延迟场景(< 1ms) |
一句话:"Disruptor 把无锁编程做到极致——RingBuffer 预分配省 GC、CAS 替代锁、缓存行填充避伪共享。但对于大多数金融业务系统,BlockingQueue 的吞吐量已经足够——支付回调每秒最多几千笔,远没到 Disruptor 的门槛。关键是知道'什么场景用什么工具'。"
核心要点回顾
AQS 是 JUC 并发包的骨架框架,核心由三部分组成:state(volatile int,语义由子类定义——ReentrantLock 为 0/1/>1 表示重入次数、Semaphore 为剩余许可数、CountDownLatch 为尚未完成的计数、ReentrantReadWriteLock 高 16 位读锁次数+低 16 位写锁重入次数)、CLH 队列变体(双向链表,通过 park/unpark 阻塞等待而非自旋空转,前驱释放后唤醒后继,保证 FIFO 公平性)、模板方法模式(tryAcquire/tryRelease 由子类实现获取与释放的具体逻辑,AQS 负责排队与调度)。Condition 精准唤醒是 synchronized+wait/notify 无法做到的——一个 Lock 可创建多个 Condition,生产者线程在 notFull 条件上等待、消费者在 notEmpty 条件上等待,signal() 仅唤醒对应条件队列中的线程,避免无效唤醒导致的上下文切换开销。
线程池执行流程遵循严格的四级递进:当前线程数 < corePoolSize → 创建核心线程(即使有空闲核心线程)→ 尝试入队(workQueue.offer)→ 队列满且线程数 < maximumPoolSize → 创建非核心线程 → 线程数达上限 → 执行拒绝策略。优先入队列而非立即创建新线程的设计意图是降低线程创建销毁开销,通过队列缓冲平滑流量。生产环境必须使用显式 ThreadPoolExecutor 构造函数并指定有界队列(ArrayBlockingQueue 天然背压),严禁使用 Executors 便捷方法——FixedThreadPool 的无界 LinkedBlockingQueue 可能导致任务堆积 OOM,CachedThreadPool 的无限线程创建可能导致线程数爆炸。参数设置按场景区分:CPU 密集型 = 核数 + 1,IO 密集型 = 核数 × 2。CompletableFuture 将异步代码从"回调金字塔"转变为"链式流水线"——关键三原则:生产必须指定自定义线程池(默认 ForkJoinPool.commonPool 被全局共享)、异常必须通过 exceptionally() 或 handle() 兜底、异步任务嵌套用 thenCompose 避免双重包装。Disruptor 是更高阶的无锁方案——RingBuffer 预分配消除 GC、CAS 替代 ReentrantLock、缓存行填充(前后各 7 个 long 凑满 64 字节)消除伪共享——吞吐量可达 BlockingQueue 的 5-10 倍,适用于金融高频交易等毫秒级延迟敏感场景,99% 的业务场景 BlockingQueue 已足够。
线程池 7 个参数是联动的方程组,不是 7 个独立变量——改一个,另外 6 个的边界条件全变。收藏这 5 条推导式原则,下次调参或面试时从"为什么"开始回答。
下一篇:《并发编程(三):ThreadLocalMap里几十万个死Entry——排查了半天OOM根因就一行finally》 系列合集:掘金Java合集