ThreadPoolExecutor的execute方法如何保证状态检查的安全性?
基础示例
// this.a是共享变量 AtomicInteger a = this.a; int x = a.get();
在多线程场景下,我们无法保证x的值为1,因为其他线程可能在我们将a的存储值加载到x之前调用a.getAndIncrement(),导致最终x的值为2。
ThreadPoolExecutor的execute方法分析
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); if (!isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } else if (!addWorker(command, false)) reject(command); }
重检查逻辑的并发疑问
聚焦核心重检查代码:
if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); if (!isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); }
我们从AtomicInteger类型的ctl读取值并加载到recheck中,但ctl随时可能被其他线程修改。那这种重检查是有绝对可靠的机制保障,还是只需要覆盖大部分场景即可?
极端并发场景分析
考虑以下低概率情况:
if (isRunning(c) && workQueue.offer(command)) { // 当前线程池处于RUNNING状态 int recheck = ctl.get(); // 读取ctl时仍为RUNNING状态 if (!isRunning(recheck) && remove(command)) // 此时线程池还是RUNNING状态,跳过分支 reject(command); else if (workerCountOf(recheck) == 0) // 读取到workerCount为0 // 此时其他线程触发线程池进入SHUTDOWN状态 addWorker(null, false); // 执行这一步会发生什么? }
结合线程池状态转换特性:线程池无法从SHUTDOWN状态回到RUNNING状态,问题可转化为:如果在重检查期间线程池被关闭,但当前线程未能读取到最新状态,会引发什么问题?
注意到添加新Worker时,addWorker方法会再次检查当前线程池状态,并且在将Worker加入workers集合前会获取mainLock。由此衍生出三个核心疑问:
- 是否是锁保证了线程池状态控制的安全性?
ctl是否是前置校验变量,用于拦截大部分并发问题?ctl变量存在的原因是否是为了避免过度获取全局锁?
问题解答
1. 锁是状态控制安全性的最终保障
是的,mainLock是线程池状态和Worker集合操作的最终安全屏障。当addWorker执行到实际添加Worker的步骤时,会先获取mainLock,然后再次检查线程池状态:如果此时线程池已经处于非RUNNING状态(比如SHUTDOWN),且没有待执行的任务,就会放弃添加Worker。
也就是说,即使前面的ctl读取是过时的,最终的锁内检查会纠正这个问题,确保不会在错误的状态下创建Worker。
2. ctl是前置校验的快速拦截器
没错,ctl作为原子变量,承担了前置快速校验的角色。它可以在不获取全局锁的情况下,快速判断当前线程池的状态和Worker数量,拦截大部分不需要进入锁逻辑的场景:
- 比如当Worker数量已经超过核心线程数时,直接进入队列逻辑;
- 当线程池已经处于非RUNNING状态时,直接尝试拒绝任务。
这种前置校验可以避免频繁获取mainLock带来的性能开销。
3. ctl的核心作用就是减少全局锁的使用
完全正确。线程池的mainLock是全局重入锁,如果所有状态判断和Worker操作都依赖锁,会导致高并发场景下的锁竞争加剧,严重影响性能。ctl将线程池状态(高3位)和Worker数量(低29位)打包存储在一个AtomicInteger中,通过原子操作实现无锁的状态读取和更新,只在必要的场景(比如添加/移除Worker、修改线程池状态)才获取mainLock,极大降低了锁的使用频率,提升了并发性能。
内容的提问来源于stack exchange,提问作者SpreadZhao

