并发入门篇更多是在帮你建立“线程不是免费的、共享状态会带来风险、线程池不是随便开几个线程”的基础意识。到了进阶阶段,重点就不再是知道几个类名,而是理解它们背后的运行逻辑,以及这些逻辑在真实系统里会怎样影响稳定性。
这一篇我们重点看四块内容:
volatile和可见性;- AQS 和并发同步器的共同底座;
CompletableFuture和异步编排;- 线程池治理与生产环境观测。
它们看上去分散,实际上分别对应并发世界里四个最常见的问题:共享变量怎么安全可见、线程怎么协作排队、异步流程怎么组织、资源池怎么不失控。
并发进阶真正难在哪里?
很多人学并发时,一开始会陷入 API 视角:会用 synchronized、会 new 线程池、会写 CompletableFuture 链式调用,好像就已经掌握了并发。可一旦系统开始承受压力,问题马上会换一种样子出现:
- 明明变量改了,另一个线程却没看到;
- 线程没有死锁,但吞吐越来越低;
- 异步流程代码看起来很现代,异常却悄悄丢了;
- 一个通用线程池承接所有任务,高峰时整个服务一起卡住。
这说明并发真正难的不是“会写”,而是“知道这些工具在什么边界下会失效,以及出了问题从哪层排查”。
volatile 解决的是可见性,不是万能同步
这是并发里最容易被误解的点之一。很多人第一次看到 volatile,会把它当成轻量版锁,觉得只要变量加了这个关键字,多线程访问就安全了。事实远没有这么简单。
volatile 真正做了什么?
从工程角度看,它主要带来两层保证:
- 一个线程对变量的写入,其他线程能更及时地看到;
- 某些危险的指令重排会被限制,从而保证可见的执行顺序。
这意味着它适合解决“状态已经被某个线程改了,但别的线程迟迟看不到”的问题。
它不能解决什么?
它不能天然保证复合操作的原子性。像下面这类典型操作:
- 自增;
- 先判断后更新;
- 多步读取再写回;
- 依赖多个共享变量的组合状态。
这些都不是单纯的可见性问题,而是读改写过程的一致性问题。单靠 volatile 不能保证安全。
什么时候适合用 volatile?
比较典型的场景包括:
- 配置刷新标记;
- 关闭开关;
- 双重检查中的状态可见性;
- 只写一次、读很多次的简单状态量。
一旦涉及复杂共享状态,应该优先考虑锁、原子类、无锁数据结构或者直接重新设计共享方式。
并发问题为什么常常和 Java 内存模型一起出现?
因为多线程问题本质上不只是“代码顺序看起来怎样”,而是“不同线程最终看到的内存结果是不是一致”。Java 内存模型关心的就是这些事情:
- 一个线程的写入什么时候对另一个线程可见;
- 编译器和处理器在什么范围内可以重排指令;
- 同步手段怎样建立可靠的 happens-before 关系。
这也是为什么真正的并发问题经常很反直觉。你在源码里看到的顺序,不一定就是多个线程观察到的顺序。所以并发进阶一定要开始习惯从“内存可见性和执行顺序”而不是“代码排版顺序”来理解问题。
AQS 为什么是 Java 并发工具类的重要底座?
很多人会用 ReentrantLock、CountDownLatch、Semaphore,但把这些工具真正串起来看时,会发现它们背后大量共享同一种设计思路,这个思路就是 AQS。
AQS 真正在做什么?
你可以把它理解成一个围绕“状态”和“排队”组织起来的同步框架。它主要解决几件事:
- 线程获取资源失败后,怎样排队等待;
- 资源何时被释放;
- 后续线程怎样被唤醒;
- 某个同步器是独占模式还是共享模式。
这也是为什么很多同步工具虽然外部语义不同,底层却能共享一套思路。因为本质上它们都在回答同一个问题:多个线程如何围绕一个有限状态安全协作。
为什么理解 AQS 有价值?
不是为了让你立刻手写同步器,而是为了帮助你理解:
- 为什么锁竞争严重时线程会进入等待队列;
- 为什么某些同步器适合共享获取,某些适合独占获取;
- 为什么一个并发工具在高压下会表现出特定吞吐特征。
当你开始理解这些底层机制,再看锁、公平性、中断、超时等能力时,就不会只停留在 API 背诵层。
锁、原子类、并发容器,应该怎么选?
这是并发工程里特别现实的问题。没有一种方案适合所有场景,真正要看的是共享状态的复杂度和冲突强度。
共享状态简单,优先考虑原子类
如果只是对单个数值或单个引用做简单原子更新,原子类通常更轻、更直接。
共享状态复杂,需要完整临界区时,锁更稳
一旦逻辑涉及多步操作、多个变量之间的一致性,锁往往更容易保证正确性。不要因为“锁听起来重”就本能回避它,错误的无锁设计通常比合理的加锁更危险。
数据结构层面已有成熟实现时,优先用并发容器
像 ConcurrentHashMap 这类结构,本质上已经帮你处理了大量并发细节。很多时候,与其自己围绕普通容器加锁,不如先看看 JDK 是否已经提供了成熟工具。
CompletableFuture 为什么不是“语法更花哨的 Future”?
因为它真正解决的不是“异步拿结果”这一件事,而是异步任务之间的编排问题。
传统 Future 的问题
传统 Future 更像是“你先提交一个任务,之后再去等它结果”。它能表达异步执行,但很难优雅地表达:
- 前一个任务完成后继续做什么;
- 两个异步结果怎样合并;
- 某个阶段失败后如何补偿或降级;
- 异常如何沿链路传播;
- 不同步骤使用哪个线程池。
CompletableFuture 的真正价值
它把异步流程从“阻塞等待”推进到了“可组合的任务图”。也就是说,你可以更自然地表达:
- 串行依赖;
- 并行聚合;
- 成功和失败分支;
- 超时控制;
- 结果转换;
- 异常恢复。
在聚合接口、批量查询、远程调用拼装、异步补偿这些场景里,这种能力非常有价值。
但它也最容易把代码写乱
因为一旦没有边界意识,链式调用就会迅速膨胀成“回调迷宫”:
- 正常路径和异常路径搅在一起;
- 同步 / 异步线程切换不清晰;
- 某一步吞掉异常,后面所有逻辑都在错误状态下继续;
- 公共线程池被大量业务异步任务挤爆。
所以 CompletableFuture 真正难的地方,不是 API 数量,而是你是否有能力控制异步链路的结构。
线程池为什么必须上升到“治理”层面?
很多系统早期用线程池没有明显问题,是因为任务量不大、调用链也短。一旦服务进入真实生产环境,线程池立刻会成为系统稳定性的关键部件。
线程池不是“有就行”
线程池背后涉及的是一整套资源调度问题:
- 任务是 CPU 密集还是 I/O 密集;
- 核心线程数和最大线程数是否合理;
- 队列长度会不会掩盖问题;
- 拒绝策略是不是与业务契合;
- 任务是否需要隔离;
- 线程池耗尽时系统怎样退化。
如果这些问题一个都不看,只是默认参数直接上,线程池很容易从“提效工具”变成“故障放大器”。
不同业务为什么要做线程池隔离?
这是非常常见、也非常重要的工程实践。因为不同任务对线程资源的占用模式完全不同:
- 查询类任务大多受外部 I/O 影响;
- 计算类任务更吃 CPU;
- 批处理类任务持续时间长;
- 通知类任务允许一定延迟;
- 核心交易任务不能被低优先级任务拖死。
如果把所有任务都丢进一个公共线程池,高峰时最先出问题的通常不是某个单点功能,而是整个服务的响应能力一起下滑。
线程池治理还包括观测与预案
一个成熟系统不会只关心“线程池配置写了没”,还会关注:
- 当前活跃线程数;
- 队列积压长度;
- 任务平均执行时间;
- 拒绝次数;
- 超时比例;
- 线程池耗尽时的降级策略。
只有看见这些数据,线程池治理才不是纸面配置,而是可持续运营的能力。
项目里怎样把这些能力串成一套并发心智?
可以按下面这个顺序理解:
1. 先减少共享,再谈同步
共享状态越少,并发复杂度越低。能通过任务拆分、不可变对象、局部变量解决的,就不要一开始就靠复杂同步。
2. 必须共享时,先判断问题是哪一类
是可见性问题、原子性问题、还是资源竞争问题?不同问题对应不同工具,不能一把梭。
3. 异步化不是目的,边界清晰才是目的
不是所有调用都值得改成异步,也不是只要异步就会更快。异步只是把等待换成了编排复杂度,前提是你能驾驭这份复杂度。
4. 线程池要按资源池来治理,而不是按工具类来理解
线程池本质上和数据库连接池、对象池一样,都是受限资源池。真正要关心的是容量、隔离、观测和退化能力。
最常见的并发进阶误区
1. 把 volatile 当成原子性方案
这会导致很多隐蔽的数据竞争问题,尤其在计数、状态迁移、组合判断场景里特别常见。
2. 过度追求“无锁”,却没有正确性证明
很多看似高级的优化,最后只是把 bug 变得更难复现。
3. CompletableFuture 写得很长,却没有明确异常策略
这类异步链一旦出问题,排查难度通常比同步流程高得多。
4. 线程池全站一个,谁都能往里丢任务
这几乎是线上性能问题的高频诱因。
5. 只盯着代码,不看监控和运行数据
并发问题很多时候不是从源代码里一眼看出来的,而是从队列积压、耗时分布、拒绝次数和 GC 压力这些运行信号里浮现出来的。
这一篇真正想帮你建立的判断习惯
当你下次遇到并发问题时,不妨按这个顺序问自己:
1. 这是可见性、原子性还是资源竞争问题?2. 当前共享状态能不能减少,而不是直接同步?3. 如果必须同步,应该用锁、原子类还是并发容器?4. 如果要异步化,异常、超时、线程池归属有没有设计清楚?5. 线程池是否有隔离、指标和退化策略?
只要这套判断顺序稳定下来,并发进阶就不再是一些零碎 API 的集合,而会变成你构建稳定系统时的基本能力。
总结
Java 并发进阶真正要学会的,不是背更多类名,而是把共享变量、同步器、异步编排和资源治理放到一个体系里看。volatile 解决的是可见性边界,AQS 解释了很多同步工具的共同底座,CompletableFuture 负责组织复杂异步流程,而线程池治理则决定这些流程在生产环境里能不能稳定运行。只要你能从“运行机制 + 资源治理”的视角来看并发,这一块能力才算真正开始落地。