JUC源码学习指南:AQS、ConcurrentHashMap与线程池核心解析 最近在整理JUC相关的源码学习笔记顺手把自己啃了一个月的ReentrantLock、AQS、ConcurrentHashMap、ThreadPoolExecutor这些核心类源码时踩过的坑和一些理解沉淀了一下。如果你正准备开始读并发源码却不知道从哪里下手或者读了一部分卡在某处回头又忘这篇应该能帮你把整个JUC的学习路线和重点模块串起来也把我踩过的那些“代码看了但没看懂”的坎儿提前告诉你。我算是那种习惯边看源码边写注释的人白天写业务代码晚上翻并发源码坚持了一个多月读完了JUC最核心的几个类。效果可以说立竿见影——原来写并发代码那种“看着能用但心里发虚”的感觉基本消失了。下文完全按实际学习顺序展开重要程度也是由地基到上层建议按我的顺序来读不要一上来就干ConcurrentHashMap的扩容那个会劝退的。1. 为什么JUC源码值得系统性读一遍先说点实际的绝大多数人写多线程代码的状态是在用别人封装好的工具类比如synchronized加锁、线程池提交任务、CountDownLatch做等待但这些工具在什么场景下容易踩坑很多人说不出所以然。JUC源码解决的正是这个问题——它把并发编程的底层逻辑、锁的实现、队列的管理、线程的状态流转全部摊开了读完以后你对“并发”的理解不再是应急背考点而是一套可以推理的系统。我自己的体会是读完JUC源码最直接的收益有三层第一层看懂工具行为背后的规则。比如ReentrantLock为什么能重入、公平锁和非公平锁到底差在哪、synchronized和Lock在底层争抢上有什么本质区别。第二层能从源码视角定位线上问题。比如线程池队列为什么总堆积、吞吐为什么上不去、某个线程为什么拿不到锁一直BLOCKED这些不再是玄学通过看源码里的逻辑就能推理。第三层提升代码设计能力。JUC里的AQS抽象、模板方法模式、队列自旋CAS、双标记位状态机这些是Java领域天花板级的设计范本读到第三遍的时候你写业务代码都会不自觉地讲究起来。这个话题适合谁呢个人觉得有两类人收益最大一类是工作两三年的服务端开发者已有的并发实践经验不少但缺少系统的底层补充另一类是准备面试、需要答得出“锁的原理和线程池的执行流程”这种深度问题的人。如果你才刚开始学Java没多久、还在写同步代码我建议你先打好Java基础再来看否则AQS的模板方法和线程池的状态机很容易劝退。2. 整体架构脉络从地基往上怎么读JUC包java.util.concurrent的类非常多如果按字母顺序读或者随机挑一个类读基本读不到三天就乱了。我趟出来的合理学习顺序是按依赖关系从下往上走这样才能保证读后面的代码时前面的概念都成了“默认已知”。我用一句话概括JUC的底层结构最底下是Unsafe和CAS、volatile这些硬件层的依赖往上一层是所有锁和同步器的抽象骨架AQS再往上是各种具体的锁、并发集合、线程池、原子类、同步工具最上层才是我们业务里天天用的那些API接口。我的学习顺序固定为五条线每条线的递进关系可以这样理解学习阶段核心模块学完能解决的问题第一阶段Unsafe、CAS、volatile、AQS锁的本质是什么线程怎么排队第二阶段ReentrantLock、ReentrantReadWriteLock、Condition局部的锁如何实现公平与重入第三阶段ConcurrentHashMap、CopyOnWriteArrayList无锁或分段思想在集合里怎么落地第四阶段ThreadPoolExecutor、ScheduledThreadPoolExecutor线程池的启动、调度、拒绝逻辑第五阶段原子类、LongAdder、CountDownLatch、Semaphore、CyclicBarrier辅助工具的内部机制到底是什么这个顺序是有讲究的。AQS是JUC锁相关类的“毛线头”只要你扯住它所有锁的源码都能顺藤摸瓜线程池的内部状态也大量用到AQS的框架支撑并发集合里则有很多不依赖锁、而是用CAS加volatile的技巧。我建议每个阶段之间停个两三天自己动手写一点小Demo验证源码行为不要贪快源码这东西一遍记住才是怪事。3. AQS源码深读整个JUC的地基AQSAbstractQueuedSynchronizer抽象队列同步器Java并发包的灵魂。ConcurrentHashMap之外我花时间最多的类就是它。读AQS之前建议先把三个前置概念彻底搞明白否则后面读ReentrantLock源码时必然一头雾水volatile变量的可见性、CAS操作的原子性、以及LockSupport里的park/unpark线程挂起和唤醒机制。这三个概念是AQS的地基。3.1 两个核心成员state和CLH等待队列AQS内部最重要的状态是private volatile int state这个int承载了加锁状态的语义。比如ReentrantLock里state表示锁重入的次数Semaphore里表示剩余许可证的数量CountDownLatch里表示还有多少个门闩没被解除。一个int在不同的子类里含义完全不同这个设计非常抽象化。另一个核心是CLH双端等待队列注意一定是“双端”。队列的节点是内部类Node每个节点封装线程引用和等待状态。没有抢到锁的线程会包装成Node节点挂到队尾前一个节点释放锁时会唤醒后一个节点。顺着这个机制就能看懂“锁的排队公平性”到底怎么控制的。3.2 acquire流程源码逻辑还原以ReentrantLock的非公平锁加锁为例第一步是compareAndSetState(0, 1)这个CAS操作直接尝试改state从0到1如果改成功了说明当前没有其他线程持锁当前线程直接获得了锁连排队都省了——这也是“非公平”的第一层含义。如果改失败说明锁正被占用这时候就得进入acquire流程。acquire是AQS提供的模板方法里面依次调用tryAcquire、addWaiter、acquireQueued三个方法tryAcquire再次尝试获取锁这里留给子类实现。ReentrantLock的nonfairTryAcquire里有个很关键的逻辑——当state为0时直接CAS当state不为0但持有锁的线程是当前线程时执行state acquire实现了重入。addWaiter如果获取失败把当前线程封装成Node通过尾插法挂到CLH队列尾部。acquireQueued进入自旋判断前驱节点是不是头节点是就再次尝试获取锁如果前驱节点的等待状态为SIGNAL当前线程就可以安心地去park挂起由前驱节点在释放锁时唤醒。这里有个细节经常被开发者误解CLH队列里的线程并不是一直在自旋转圈。如果一直是死循环自旋CPU开销早爆了。真实策略是有限自旋——前驱节点状态需要置为SIGNAL之后当前线程就执行LockSupport.park主动让出CPU挂起等待唤醒。自旋和个人唤醒搭配既避免了无谓的系统调用又保证了公平唤醒的及时性。3.3 release流程源码逻辑还原释放锁走release(int arg)方法核心分两步first是调用tryRelease把state做减法如果是完全释放state减到0就把当前持锁线程置空并返回true返回true后调用unparkSuccessor唤醒CLH队列里第一个还活着的线程节点。可能有人要问“第一个活着的节点”是什么意思因为在队列里可能有一些线程节点已经因中断被取消了所以unparkSuccessor是从队头往后遍历找第一个状态不是CANCELLED的节点来唤醒。这也是后面分析“锁中断”和“锁超时”机制的入口。AQS还有一个增强版本——acquireInterruptibly和tryAcquireNanos。前者在park期间响应线程中断后者加入了超时时间超出时间阈值直接放弃排队。这两个方法在业务里用到的场景很多比如限时等待数据库连接。读源码时建议和acquireQueued对照着看差异就是在自旋过程中多了中断检查和deadline判断。读到这里AQS的骨架就算真正搭建起来了。我给个小建议读AQS时不要只看一个类最好把ReentrantLock里的Sync、FairSync、NonfairSync几个内部类的代码贴在一起对照你会发现模板方法模式被运用得极其清晰。4. ReentrantLock与Condition源码细读有了AQS的底子ReentrantLock的源码就变得非常顺畅了。它有公平锁和非公平锁两个静态内部类实现内部都依赖Sync抽象类去继承AQS。分开谈一下它们的关键差异。4.1 公平锁和非公平锁在源码上的本质区别非公平锁的lock方法第一步就直接CAS抢锁抢不到才进队列这就是“非公平”的入口而公平锁的lock方法直接调acquire在该方法内部的tryAcquire实现里会先检查一句话hasQueuedPredecessors()。这个方法探一下CLH队列如果队列里已经有排队的节点公平锁绝对不会插队只能乖乖去排队。所以公平和非公平翻译成源码逻辑就一句话非公平锁在入队之前多了一次尝试CAS的插队机会。理想情况下非公平锁的吞吐量更高因为减少了线程上下文切换但极端情况下如果一个线程反复CAS成功队列后面的线程可能长时间无法获取锁造成“饥饿”。源码注释里还专门提到了这一点建议读读注释原文里面解释得比任何课程都透彻。4.2 lock和unlock的对称性ReentrantLock加锁成功后state变成了1在同一线程中再次调lockstate变成2释放时每调一次unlockstate减1直到state归零才真正释放锁唤醒队列中的后继线程。这也是“重入锁”概念的源码级来源。理解了这个对称性线上偶尔看到监控里某个锁的state值异常高第一反应就可以猜到可能是写代码时在递归或循环里重复加锁却少了一次释放。4.3 Condition源码的等待通知机制Condition在源码里是一套独立的等待队列机制。每次调用await当前线程会从AQS主等待队列中摘除包装成一个新的Node放到Condition的等待队列中然后释放锁、park挂起。直到其他线程调用signalCondition等待队列的第一个节点被转移到AQS主队列尾部才会重新参与锁的竞争。这里特别需要注意一个坑signal只是把线程从Condition队列转到AQS队列并不代表该线程立刻获得锁。它还需要参与锁竞争竞争成功才能真正从await返回。这一长串因果如果没读源码很容易把“唤醒”和“获取锁”混为一谈。实际写码时凡是用了await和signal做跨线程通信的一定得在循环里判断条件不能单凭一次唤醒就认为状态满足这个从源码里推导出来的结论比任何经验总结都可靠。5. ConcurrentHashMap源码拆解并发集合的重头戏如果AQS是地基那ConcurrentHashMap就是JUC集合里最硬核的一座山。JDK 8和JDK 7的实现思路差异很大我直接以JDK 8的源码为学习目标因为线上主流版本就是它。5.1 JDK 8的并发设计思路CAS synchronized的组合拳JDK 7的ConcurrentHashMap用的是分段锁Segment数组每个Segment是一把锁JDK 8之后把它彻底抛弃改为“CAS抢槽位 synchronized锁链表头节点或树根节点”的模式。这个改进让锁粒度从多个Segment变成单个桶并发能力大幅提升。putVal方法的核心执行顺序可以用四步概括第一步检查table是否为空为空则初始化。初始化时通过sizeCtl变量的CAS竞争保证只有一个线程完成初始化。第二步根据hash找对应桶下标。如果这个桶为空直接CAS把Node放进去这一步是无锁的性能极高。第三步如果桶下标处的节点hash值为-1说明当前正在扩容插入线程会帮助扩容ForwardingNode机制。第四步如果桶不空且没有在扩容那就synchronized锁住这个桶的头节点然后往链表或红黑树中插入或替换节点。链表长度超过8且数组长度达到64时链表转红黑树。可以看到JDK 8的优化核心就是“能无锁就无锁不能无锁就把锁的粒度缩到最小”。这个设计里最值得学的是ForwardingNode这种思想——扩容过程中旧table的每个桶会被替换成一个转发节点读写线程遇到它就知道该去新table找数据同时还能帮一把扩容迁移。这不是业务代码里能常规设计出来的方案但读完以后对我做状态迁移类的架构设计启发很大。5.2 扩容机制源码必须关注哪几行扩容是ConcurrentHashMap里最复杂的逻辑。由单线程完成旧桶迁移协助迁移的线程通过transferIndexCAS方式分配任务区间每个线程领取一个stride区间的桶迁移任务谁也不跟谁冲突。源码里有一个nextTable变量指向扩容后的新数组。迁移过程中读操作还是可以在旧表中进行遇到ForwardingNode就跳到新表写操作则需要锁住旧桶头节点完成迁移不让读写并发产生脏数据。整套机制读完你心里会对并发集合的“在线扩容”有一个非常具象的理解是“让我帮你一把”而不是“锁住全部”。时机的选择上扩容阈值是0.75 * 数组长度这一点和HashMap是一致的但实现细节差异极大。读ConcurrentHashMap的时候建议把JDK 7的Segment版本也简单过一遍不用读太细能对比出两者的设计思路就行。面试如果问到“分段锁为什么被抛弃”有对比记忆会答得更准确。6. ThreadPoolExecutor源码的关键知识点线程池在业务代码里出场率极高但很多人对参数背后的行为细节并不清楚。我建议把ThreadPoolExecutor源码重点读三块状态定义、execute流程、拒绝策略触发点。6.1 ThreadPoolExecutor的状态机和那个精妙的int线程池的运行状态用一个int变量ctl来表示内容分两段高3位是运行状态RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED低29位是工作线程数workerCount。这是非常经典的一个int字段承载两个信息的位运算设计。运行状态之间的转换逻辑和执行execute提交的线程数判断是强相关的。比如调用shutdown后状态变成SHUTDOWN此时不再接收新任务但会等待任务队列里已有的任务处理完调用shutdownNow则状态变成STOP直接中断所有正在执行的线程并返回队列中未处理的任务列表。这个语义在源码注释里直接写明读完之后你写优雅停机代码时心里非常有底。6.2 execute提交任务的执行路线execute(Runnable command)的执行流程我总结了四步判断检查当前工作线程数是否小于核心线程数corePoolSize是则新建一个Worker线程执行任务。如果线程数已经达到corePoolSize尝试把任务丢进工作队列workQueue丢成功后再二次检查线程池状态和线程数防止线程池在入队瞬间被关闭或线程意外死亡。如果队列也满了尝试新建非核心线程执行任务但总数不能超过maximumPoolSize。如果超过maximumPoolSize则走拒绝策略默认就是抛RejectedExecutionException。这个顺序是线程池设计的精髓——逻辑判断的顺序直接决定了任务是被立即执行、进队列等待、还是被拒绝。实际很多业务里线程池队列满了被拒绝其实是因为最大线程数设置不合理或者队列容量太小读了源码之后再去调整这两个参数就不会是拍脑袋了。6.3 四种拒绝策略源码侧对比策略内部行为AbortPolicy直接抛RejectedExecutionException默认策略CallerRunsPolicy谁提交的任务谁自己执行也就是提交任务的线程直接run该任务DiscardPolicy静默丢弃不抛异常DiscardOldestPolicy丢弃队列头部的老任务再重新提交新任务这四个策略类都只有简短的方法体但使用场景差别很大。比如想让任务不丢又能限流CallerRunsPolicy非常合适如果允许丢弃部分不重要日志DiscardPolicy也够用。源码里每个策略实现都只有三两行建议手动敲一遍你就能记住“线程池拒绝策略不是只有抛异常这一种”。Worker线程本身的运行机制也建议读一下每个Worker都实现了AQS它既能表示一个线程又承载了锁的语义用来在shutdown的时候判断线程是否空闲。这里的双重身份也是JUC源码里非常惊艳的设计点。7. 原子类与LongAdderCAS高并发优化思路原子类这块AtomicInteger的源码短平快核心就是Unsafe.getAndAddInt里面死循环CAS直到成功。如果没有竞争一次就能成功高竞争下会反复失败重试这也暴露了CAS的一个短板——竞争越激烈重试次数越多CPU开销越大。LongAdder就是为了解决高竞争下CAS自旋开销大的问题而出现的。核心思路很简单维护一个base变量加一个Cell数组。多线程写入时先通过hash散列到不同的Cell里各自累加最后汇总sum时把base和所有Cell的值加在一起。这本质上就是“分段累加”思想每个线程只需在自己的Cell上CAS冲突概率大幅下降。读LongAdder源码时建议连带读一下Striped64这个父类里面关于Cell数组的初始化和扩容逻辑条件很细节。比如通过getProbe取线程的hash值如果当前Cell为空就尝试初始化Cell数组满到一定程度还会扩容。这套设计比我以前以为的“LongAdder就是简单分几个桶”要复杂得多也科学得多。实际使用上如果你的场景是并发量极高的统计计数如请求数、访问量累计LongAdder比AtomicInteger性能高出不少如果是一场交易里只需要简单判断“等于某值再更新”AtomicInteger的语义更直接。两者各有用处不要片面替代。8. 三个同步工具类的源码差异CountDownLatch、Semaphore、CyclicBarrier很多初学者把这三个工具搞混读源码就能彻底区分开它们的机制CountDownLatch和Semaphore是基于AQS的共享锁实现CyclicBarrier则完全不是一个路子它是基于ReentrantLock和Condition实现的。CountDownLatch的countDown就是释放一次共享锁内部把state减1await就是一直死等state归零且中断或超时才返回。Semaphore的acquire是state减1release是state加1相当于许可证的发放和回收。这两者在AQS里的tryAcquireShared和tryReleaseShared方法实现完全不一样前者是“是否计数归零”后者是“是否还有剩余许可”很直观。CyclicBarrier就比较特别了。它的实现里有个lockReentrantLock加上一个condition每个线程到达屏障点时通过await将计数减1如果还没到总数就condition.await挂起。当最后一个线程到达计数归零它负责执行可选的barrierAction任务并调用condition.signalAll唤醒所有等待线程。这个类之所以叫Cyclic是因为barrier计数归零后可以重置重用而CountDownLatch用完就废了。业务选择上也容易区分等待多个线程都完成某件事之后一起继续用CyclicBarrier如数据分片加载完成后合并一个或多个线程等待其他计数完成用CountDownLatch如主线程等所有子任务完成控制并发访问量时用Semaphore如限流连接池。这些结论读源码时会自然顺手得到不需要死记。9. 源码学习中最容易踩的坑和排查技巧这块算是我一个月趟雷过程中的精华分享几条实用的经验。源码读不下去或者读完就忘大多是下面几个原因。第一坑死记源码行号不记逻辑脉络。源码是在升级的我和几个朋友在学习时经常对着老版本的讲解去JDK新版代码里找同一个方法结果完全对不上。正确姿势是记“方法间调用关系”比如acquire - tryAcquire - addWaiter - acquireQueued这个链条比具体行号稳定得多。阅读工具上我建议用Idea的调用层级快捷键CtrlAltH来追踪调用链比肉眼逐行找高效很多。第二坑只读源码不验证。源码里看到结论比如非公平锁插队、ConcurrentHashMap扩容协助如果不去实际写Demo验证记忆很难持续。我习惯在每个模块学完后写一个验证小程序比如自己用AQS实现一个简单的独占锁或者打印ThreadPoolExecutor执行过程中workerCount的变化。手写一个简易AQS锁的过程会让整个AQS的理解固化下来。第三坑上来就读大型方法。比如ConcurrentHashMap的helpTransfer、transfer这些方法动辄一两百行新手直接看会死得很惨。建议先读小方法如tabAt、casTabAt、setTabAt把JDK内部依赖Unsafe做的原子操作基础结构搞清楚再逐步进入大方法。大方法里复杂的逻辑我会拆成一个一个if分支来看每次只看一个分支配合断点观察。第四坑忽略Debug工具的使用。Idea里调试多线程源码时建议重点用“Evaluate Expression”和条件断点特别是AQS的CLH队列节点状态变化条件断点设成“node.prev null”之类非常容易看出队列入队出队的全过程。还有Mutex这类AQS官方示例类特别适合作为第一个自己调试的AQS子类代码只有几十行看完立刻对AQS全流程有底。排查线上并发问题也一样会读源码以后多线程堆栈里出现的parkAndCheckInterrupt、acquireQueued这类字眼就不会是乱码了你可以直接判断出线程是在等锁还是在被挂起。再配合jstack观察线程状态定位并发问题会快很多。10. 一个可以抄作业的学习Demo手写简易AQS锁为了巩固AQS的理解我强烈建议在读完AQS后自己实现一个最简独占锁。这样可以把模板方法流程真正吃透。这里给出我当时练习的核心代码框架你可以自己填实现。public class SimpleLock { private final Sync sync new Sync(); public void lock() { sync.acquire(1); } public void unlock() { sync.release(1); } static class Sync extends AbstractQueuedSynchronizer { Override protected boolean tryAcquire(int arg) { if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } Override protected boolean tryRelease(int arg) { if (getState() 0) { throw new IllegalMonitorStateException(锁未持有); } setExclusiveOwnerThread(null); setState(0); return true; } Override protected boolean isHeldExclusively() { return getState() 1; } } }这段代码建议自己敲一遍然后替换成公平锁版本也就是在tryAcquire里加上hasQueuedPredecessors()检查。实测下来第一次跑通一个自制锁的那一刻AQS的内部队列机制会从“理解”变成“肌肉记忆”。调试时可以在acquireQueued的循环条件上打条件断点观察自定义线程进入队列和出队的过程。如果手边有多个线程同时竞争锁打开Idea的线程视图能看到等待线程真实处于WAITING (parking)状态——这个视觉记忆比读十遍书都管用。从AQS敲到自己的自定义锁对LockSupport的park/unpark理解会同时加深后面再去看Semaphore和CountDownLatch你会发现代码大同小异因为它们的骨架都是共享模式的两三个回调方法。真正难的永远是抽象那下子而不是代码量。根据我个人的学习经验JUC源码适合反复读、隔段时间回炉一遍。第一次重点看AQS和ReentrantLock把主线打通第二次带着业务问题去读ConcurrentHashMap和线程池第三次再回来看同步工具类和原子类基本就是查漏补缺了。源码这东西越早开始啃越好啃完再看并发相关的面试题和线上问题会发现一切都在那里等着你。