2026阿里并发编程全优笔记:从JMM到线程池的实战梳理

如果你啃过几本并发编程相关的书,也刷过一些技术博客,大概率会有和我当时一样的感受:每个知识点单独拎出来好像都认识,synchronized 知道、volatile 也听说过,可一旦要把它们组合起来解决线上问题,脑子里的知识立刻变成一团浆糊。我真正把“并发编程”这几个字吃透,靠的不是刷了多少篇文章,而是把一份体系完整的“阿里并发编程全优笔记(2026最新版)”从头到尾过了三遍,再配合大量自己写的样例代码,才慢慢把散落的知识点串成一张网。今天这篇文章,就基于这份笔记的框架,结合我多年写并发代码的真实经验,把并发编程这条主线的核心内容完整梳理一遍。

这份笔记不是什么速成鸡汤,它更像一张由一线工程师反复打磨出来的并发知识地图,覆盖 JMM、锁、并发容器、线程池、异步编排、常见故障排查这些关键节点,特别适合三类人看:刚接触并发、想建立体系化认知的 Java 后端开发;写 CRUD 写腻了、想在技术上往上走一步的工程师;以及系统已经出现过并发问题、想系统排查和预防的运维或者全栈同学。接下来我按照笔记的逻辑,把每块内容掰开揉碎,顺带把我踩过的坑和验证过有效的做法一并写出来。

1. 并发编程笔记的整体设计与学习思路

1.1 先明确“全优笔记”到底在讲什么

网上流传的“阿里并发编程全优笔记”,其实是一套被反复整理、多次迭代过的 Java 并发编程学习资料,核心源自一线工程师在真实业务场景里的沉淀。2026 最新版相比早年间流传的版本,最大的变化是融入了新版 JDK(如 JDK 17、JDK 21)下的实践差异,比如对虚拟线程的铺垫性介绍、对synchronized在现代 JVM 中表现的重新评估,以及对 CompletableFuture 异步编排更深入的落地案例。

笔记的价值不在于告诉你有多少知识点,而在于它对知识点做了分层:底层是 Java 内存模型(JMM)和并发理论,中间是锁、并发容器、工具类这些基础设施,顶层才是线程池、异步编排、高并发系统设计等实战内容。这种分层方式和人的认知习惯是匹配的,先弄清楚底层“为什么”,再去看上层“怎么做”,才能做到真正理解而不是死记硬背。

1.2 并发编程为什么容易学成“夹生饭”

我见过不少同学学习了几个月并发,依然搞不清楚这样的问题:为什么加了volatile变量还是被多线程改乱了?为什么HashMap在多线程下会死循环?为什么明明用了线程池,系统还是被拖垮?

答案都可以归结到并发编程的三大根源:可见性、原子性、有序性。CPU 缓存和主内存之间的一致性需要协议来保证,这是可见性问题的来源;CPU 为提升效率会乱序执行指令,这是有序性问题的来源;多线程切换导致一段代码被拆成多条指令交错执行,这是原子性问题的来源。

要理解这三个问题,可以想象一个多人协作的厨房:你负责切菜,我负责炒菜,他负责摆盘。每个人都有自己的小台面(CPU 缓存),共享一个大冰箱(主内存)。如果切菜的人把切好的菜放在自己台面上没有及时放到冰箱,炒菜的人去冰箱里取就会扑空,这是可见性问题;如果三个人都觉得自己应该最后摆盘,指令顺序就乱了,这是有序性问题;如果两个人同时去冰箱里取同一条鱼,大家都想“先取出鱼再放回去”,两个操作交错执行就可能出岔子,这是原子性问题。并发编程的一切工具和技巧,本质上都是在解决这三个问题。

这份笔记的聪明之处,是它没有一上来就让你背并发工具类 API,而是反复强调 JMM 和“三大问题”这个底层坐标系。当你理解了一切的工具都是为这三大问题服务的,再回头看锁、CAS、并发容器,思路会顺非常多。

2. 核心知识体系拆解:JMM、锁、容器、线程池

2.1 先从线程生命周期和 JMM 说起

笔记中第一条主线是线程本身。Java 里创建线程的方式有很多种,Thread、Runnable、Callable、FutureTask、线程池,本质上最后都是创建一个Thread对象并调用start()。线程的状态机(NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED)看着简单,但很多线上问题都出在状态转换上,比如wait()和notify()使用不当,线程就一直卡在 WAITING 状态不干活。

JMM(Java 内存模型)是并行编程的地基。它的核心规定是:每个线程有自己的工作内存(对应 CPU 缓存),线程对共享变量的操作必须先拷贝到工作内存,操作完成后再刷回主内存。这就产生了一个危险的空档期——一个线程改了变量还没刷回主内存,另一个线程读到的就是旧值。

JMM 为了约束这种不确定性,定义了 happens-before 规则:比如“一个锁的解锁 happens-before 后面对这个锁的加锁”,所以同一个锁保护的代码块天然不会被并发破坏;“对 volatile 字段的写入 happens-before 后续对该字段的读取”,所以volatile能保证可见性但不保证原子性。我一开始不理解为什么volatile不能保证原子性,直到用两个线程各执行 10000 次count++才发现,count++根本不是一条指令,而是“读取-计算-写回”三步,volatile只能保证三步过程中别人改的值对我可见,但不能阻止三步之间交错进行。

2.2 锁的选择:从 synchronized 到 JUC 显式锁

锁是解决原子性最直接的武器。Java 里的锁体系很庞大,但核心选择无非三个层次。

第一层是synchronized,这是 JVM 内置锁,经过多次优化(偏向锁、轻量级锁、重量级锁的升级过程)后,大多数场景下性能已经不差。笔记里特别提醒:很多人对synchronized的印象还停留在“重量级锁”时代,实际上 HotSpot 会通过锁消除、锁粗化来优化它,在低竞争场景下它甚至比ReentrantLock更轻。

第二层是ReentrantLock,它提供的功能更丰富:可中断等待、公平锁、多个Condition队列、tryLock带超时尝试等。这些能力在解决死锁、实现精细控制时非常有用。比如tryLock(3, TimeUnit.SECONDS)这种写法,拿到锁就继续,拿不到就放弃或走降级逻辑,可以极大降低死锁概率。

第三层是读写锁ReentrantReadWriteLock和StampedLock。读多写少的场景,用读写锁能把并发度拉高一大截;StampedLock则更进一步,读操作完全乐观,只有在写操作冲突时才升级为悲观锁。

选择建议也很简单:能用synchronized就不要引入额外复杂度;需要超时、可中断、公平性时才用ReentrantLock;多读少写用读写锁,但写非常少且性能极度敏感时再考虑StampedLock——毕竟它的 API 更容易用出错。

2.3 并发容器:ConcurrentHashMap 不是万能的

笔记中另一个重点模块是并发容器。很多人的第一反应是“并发场景下用 ConcurrentHashMap 就完事了”,但这个结论只说对了一半。

ConcurrentHashMap在 JDK 1.8 后的实现是用 CAS +synchronized锁住桶的头节点,锁粒度比 JDK 1.7 的段锁更细,并发度更高。但它有几个“不为人知”的细节:size()方法返回的是一个估计值,不是实时精确值;putIfAbsent这种复合操作是原子的,但“先查后改”这种业务逻辑,依然需要自己保证原子性。

除了ConcurrentHashMap,笔记还花了不少篇幅讲CopyOnWriteArrayList、BlockingQueue以及并发工具类CountDownLatch、CyclicBarrier、Semaphore。我印象最深的是这三个工具类的语义差异,特别容易搞混。

CountDownLatch是一次性的倒数门闩,适合主线程等一批任务完成;CyclicBarrier是可循环使用的屏障,适合一批线程互相等待齐头并进,比如分片处理数据时每个线程处理完一片后等大家到齐再进入下一轮;Semaphore则是信号量限流,适合控制同时访问某个资源的线程数,比如数据库连接池。我早期写多线程分片处理时用了CountDownLatch,下一次循环就发现不能复用,换成CyclicBarrier才解决,从那次以后我见到这两个类就会下意识地确认一遍业务场景再下手。

2.4 线程池:参数不是背出来的,是算出来的

线程池是并发编程从理论走向实战的必经之路,也是笔记中最长的一章。ThreadPoolExecutor的七大参数——核心线程数、最大线程数、空闲存活时间、时间单位、工作队列、线程工厂、拒绝策略——每一个都值得仔细研究。

执行流程可以简化成一句话:任务先提交给核心线程,核心线程满了进队列,队列满了拉起最大线程数的额外线程,连最大线程都拉满以后触发拒绝策略。很多线上事故都是这个流程没理解透导致的:比如把队列设置成无界队列,最大线程数形同虚设,任务无限堆积,最终内存溢出让整个应用崩溃。

拒绝策略有四种默认实现:AbortPolicy抛异常、CallerRunsPolicy让提交任务的线程自己执行、DiscardPolicy直接丢弃、DiscardOldestPolicy丢弃最老的任务。我自己最常用的是CallerRunsPolicy,因为它的效果是“牺牲调用者,保住任务本身”,在流量高峰期天然起到了 feedback 限流的作用,而不是简单粗暴地把请求丢掉。

3. 必须动手实践的几类并发模型

3.1 线程池参数计算的完整示例

参数不能拍脑袋定,我给一个自己实际用过的场景。假设一个 8 核服务器上部署订单导出服务,每个导出任务需要占用 CPU 处理 50ms,等待数据库和网络 IO 消耗 150ms,一个任务总时长约 200ms。

这里遵循一个经验公式:IO 密集型的核心线程数 = CPU 核数 × (1 + IO 等待时间 / CPU 计算时间)。代入数值就是:

8 × (1 + 150 / 50) = 8 × 4 = 32

所以核心线程数可以设为 32。最大线程数一般设置为核心线程数的 1.5 到 2 倍,这里取 48。队列容量怎么定?假设系统期望积压的任务最多让用户多等 5 秒,那么队列容量 = 5 秒 × 每秒的峰值任务提交数。如果峰值是每秒 40 个任务,队列容量可以设置 200。注意这个公式算完之后还要压测验证,因为真实系统的 CPU 消耗和 IO 等待并不是恒定不变的。

另外一个容易忽略的参数是threadFactory。一定要给线程池里的线程设置一个有意义的名字前缀,比如order-export-thread-。排查线上问题时,线程名会直接出现在jstack输出里,命名清晰的话一眼就能看出是哪个线程池在忙,能节省大量排查时间。

3.2 并发容器的选型视角:从读写比例出发

选容器不是看广告,而是看场景的读写比例和一致性要求。

读多写少、集合大小不大,优先考虑CopyOnWriteArrayList。它读操作不加锁,写操作是复制整个数组再替换引用,在配置项管理这类场景下非常好用。但你要是用它存储频繁变化的缓存数据,会频繁触发数组复制,GC 压力剧增。

写多读少或者读写压力都很大,用ConcurrentHashMap或ConcurrentSkipListMap。需要按 key 排序就用跳表,不需要就用哈希表。注意ConcurrentSkipListMap的并发度通常不如ConcurrentHashMap,因为跳表操作链路更长。

单机场景的并发控制还需要注意一个点:ConcurrentHashMap只保证单个方法原子性,不保证组合动作原子性。比如“if (map.containsKey(key)) { map.put(key, value) }”在多线程下就是有问题的,需要改成computeIfAbsent这类原子方法。这个坑我在做本地缓存时踩过,当时数据偶发不一致,排查半天才发现是“先判断后写入”这个组合动作出了问题。

3.3 异步编排:CompletableFuture 替代“回调地狱”

高并发系统里,异步化是提升吞吐量最直接的手段。早期 Java 用Future做异步,但Future的get()是阻塞的,业务逻辑一旦复杂,就会陷入回调嵌套,代码读起来非常痛苦。

笔记中花了很大篇幅讲CompletableFuture,我认为它最值钱的能力是任务编排。举一个典型的下单场景:查询用户信息、校验库存、计算优惠、创建订单、生成支付链接,这五个步骤中前三个互不依赖,可以并发执行,后两个依赖前面结果顺序执行。

用CompletableFuture的写法大概是:先定义三个异步任务分别查用户信息、校验库存、计算优惠,然后用thenCombine把用户信息和库存结果合并,再用thenCompose串起后续的创建订单和生成支付链接逻辑。一旦用了这种编排方式,接口总耗时就不再是五个步骤的耗时总和,而是“最慢的一个并行分支耗时 + 串行部分耗时”,吞吐量提升立竿见影。

需要注意两点:第一,给CompletableFuture显式传入自定义线程池,不要用默认的ForkJoinPool.commonPool(),因为公共线程池的线程数等于 CPU 核数减一,高并发下很容易打满;第二,异步任务里一定要处理异常分支,用exceptionally或者handle兜底,否则异常会静默吞掉,接口白等半天返回一个奇怪结果。

4. 高频故障与排查思路实录

4.1 死锁的定位与恢复

死锁是并发编程里最经典的故障之一。产生死锁需要四个条件同时满足:互斥、持有并等待、不可剥夺、循环等待。我在实际项目中遇到过一个很典型的案例:线程 A 持有订单锁后去获取库存锁,线程 B 持有库存锁后去获取订单锁,两个线程互相等待对方释放,谁也跑不动。

排查步骤很固定:先用jps找到 Java 进程 pid,再用jstack pid打印线程栈,日志里会直接出现Found one Java-level deadlock并列出参与死锁的线程。解决死锁的办法通常有三类:一是调整加锁顺序,所有人都按照“先订单后库存”的顺序加锁;二是用tryLock加超时,拿不到锁就回滚或重试;三是缩小锁的范围,不要在持锁期间做耗时操作,比如不要在锁内调远程接口。

4.2 线程池队列积压与线程泄漏

线程池最隐蔽的问题不是拒绝策略,而是队列积压。现象是请求越来越慢,但线程数和 CPU 都很正常,因为任务都堵在队列里排队。这种问题的排查路径一般是三步:先看监控中线程池活跃线程数和队列大小,确认是不是队列持续增长;再打印线程栈看线程都在干什么,是不是有线程卡在某个 IO 调用上;最后追查任务本身的耗时分布,看是不是某个下游依赖变慢拖垮了整体。

线程泄漏则是另一种折磨。典型场景是业务代码里写了while (true)循环去消费消息,循环没有退出条件,导致线程池里大量线程永远处于 RUNNABLE 状态,线程数缓慢爬升,最终触发拒绝策略。还有一种常见泄漏是对ThreadLocal使用不当,线程池里的线程是复用的,如果往ThreadLocal里存了对象却不清理,下一次任务复用线程时会读到上一次任务的脏数据,甚至因为强引用一直存在导致内存泄漏。解决方案就是两个:用完立刻remove(),以及不要用ThreadLocal存大对象。

4.3 伪共享:一个容易被忽略的性能刺客

伪共享是一个比较底层的并发性能问题。CPU 从主内存读数据时,并不是按变量逐个读,而是按缓存行(通常是 64 字节)批量读入。如果两个线程修改的是同一缓存行里的不同变量,哪怕这两个变量毫无关系,CPU 也会因为缓存行失效而频繁同步,导致性能大幅下降。

我之前在写一个统计组件时定义了一个数组:long[] counters = new long[8],八个线程各自独立更新counters[i],理论上应该互不干扰,结果性能怎么压都上不去。后来用缓存行填充的方式把每个变量隔开到不同缓存行,性能才有明显改善。Java 里可以用@Contended注解做缓存行填充,不过要在启动参数里加上-XX:-RestrictContended才生效。这种问题在数据量小但访问频率极高的场景(比如计数器、多线程统计数据帧)里最容易冒头。

4.4 常见并发故障速查表

问题现象可能原因排查工具/命令处理思路
接口随机超时,CPU 不高线程池队列积压线程池监控、jstack检查队列大小、下游耗时、核心线程数
死锁,进程假死加锁顺序不一致jstack 看 deadlock 段统一加锁顺序、tryLock 超时
数据偶发不一致组合操作非原子代码审查、压测复现改用复合原子方法或加锁
线程数持续上涨任务异常未退出jstack 统计线程状态定位长期 RUNNABLE 线程,检查循环退出条件
内存逐渐涨满ThreadLocal 未清理heap dump、MAT使用后 remove(),避免存储大对象
并发度上不去伪共享压测对比、perf缓存行填充、重新设计数据结构

5. 给新手的进阶路线与常见弯路

5.1 从“会用 API”到“能调性能”的方法

我推荐的学习路径分成五个阶段,前两个阶段是建立基础,后三个阶段是冲向实战。

第一个阶段,把线程和 JMM 啃透。线程怎么创建、状态怎么转换、synchronized和volatile的本质区别,这些是万层高楼的钢筋水泥。

第二个阶段,用熟 JUC 工具类。ReentrantLock、CountDownLatch、CyclicBarrier、Semaphore、ConcurrentHashMap,每个都写一个 demo,模仿真实业务去用,比如用Semaphore实现一个简单的连接池限流。

第三个阶段,聚焦线程池和异步编排。把我上面说的线程池参数计算方式实践一遍,尝试用CompletableFuture改写一个同步慢接口,对比耗时数据。

第四个阶段,深入底层原理。试着读ConcurrentHashMap的 put 源码、AQS 的 acquire 流程、ThreadPoolExecutor的 execute 流程,不需要逐行读懂,但关键方法的流程要有能力画出来。

第五个阶段,结合性能分析工具做调优。jstack、jstat、arthas、VisualVM这些工具至少熟练掌握两三个,遇到线上问题时能用工具定位而不是靠猜。到这个阶段,并发编程就算真正入了门。

5.2 学习时最容易走的弯路

一个很常见的弯路是“只背八股不写代码”。网上关于并发编程的面试题非常多,很多人能流利背诵 volatile 的可见性原理、synchronized 的锁升级过程,但让他写一个用多线程并发拉取接口并汇总结果的 demo,就不知道从哪下手。八股是结果,不是起点,写代码才是理解原理的唯一途径。

另一个弯路是“过早深钻源码”。我以前就犯过这个错误,刚学完 JUC 容器就去啃 AQS 的源码,结果在 acquireQueued 的循环里转不出来,越看越挫败。正确做法是先用、再读、再优化,等自己踩过坑之后,看源码时才会产生“原来是这样解决我的问题”的共鸣感。

如果看书,我会推荐两本做互补阅读:一本偏全面系统,适合建立知识地图;另一本偏并发实战,适合在有一定基础后深度学习锁、并发容器、执行框架这些核心机制的底层设计。记住一点:这类经典书没有 PDF 捷径,认认真真读纸质版或者正版电子书,边读边写代码做笔记,效果远好于收藏一堆资源链接。

最后分享一点我的个人体会

这几年带过一些新人,也帮朋友排查过不少线上并发问题,我最大的感受是:并发编程这门手艺,不是靠看内容看会的,而是靠一遍遍写 demo、一次次踩坑试错的“肌肉记忆”。那套阿里并发编程全优笔记最打动我的地方,是它把最容易让人望而生畏的底层机制拆解得足够清晰,让我按图索骥地把知识点一个一个落到代码里验证。

如果你也打算好好攻克并发这一关,我建议你给自己定一个目标:把线程池参数计算、CompletableFuture 编排、ConcurrentHashMap 原子方法这三个高频场景写成完整可运行的代示例放在自己的代码库里。遇到新情况就回来翻笔记、翻源码、跑测试去验证。等你能用自己的话把“为什么这个方案在这个场景下会出问题、换另一个方案为什么就好了”讲清楚,并发编程这块硬骨头,就算真正啃下来了。