目录

前言:所有Java并发问题的唯一总根源

一、终极底层根源:操作系统抢占式线程调度(所有时序问题的元凶)

1.1 核心特性

1.2 直接衍生的所有并发问题(100%核心来源)

问题1:竞态条件 → 破坏原子性(数据错乱)

问题2:持有锁线程中途暂停 → 并发阻塞、性能暴跌

问题3:唤醒/等待时序颠倒 → 信号丢失(wait/notify致命bug)

问题4:操作系统虚假唤醒 → 无理由唤醒等待线程

问题5:队列线程唤醒不立即执行 → 并发滞后

二、硬件层根源:多核CPU缓存架构 → 可见性问题

2.1 硬件架构特性

2.2 直接衍生:缓存可见性问题

2.3 底层辅助诱因

2.4 解决方案

三、软硬件优化根源:指令重排 → 有序性问题

3.1 重排来源(两层重排)

3.2 经典bug案例

3.3 解决方案

四、三大根源汇总 → JMM三大特性缺失(并发问题总结)

五、衍生高阶并发问题(全部溯源)

5.1 wait/notify 缺陷

5.2 AQS & Condition 设计意义

5.3 锁升级设计(偏向→轻量→重量)

六、终极闭环总结


前言:所有Java并发问题的唯一总根源

Java并发没有任何神秘bug,所有问题,全部来自三层底层不确定性,自上而下层层放大,最终形成我们开发中遇到的原子性、可见性、有序性、线程竞态、信号丢失等所有并发问题。

三层根源(从底层到上层,缺一不可):

  1. 硬件层:多核CPU缓存架构 + CPU指令乱序执行

  2. 系统层:操作系统抢占式线程调度(核心终极根源)

  3. 软件层:JIT编译器指令重排优化

所有Java并发技术:volatile、synchronized、CAS、AQS、LockSupport、wait/notify、Condition,全部都是为了屏蔽这三层不确定性,人为给混乱的并发执行建立有序规则。


一、终极底层根源:操作系统抢占式线程调度(所有时序问题的元凶)

1.1 核心特性

Java线程是1:1映射操作系统原生线程,JVM无权调度线程,全权由操作系统内核管控:

  • CPU采用时间片抢占机制,线程分配固定短时CPU时间片

  • 时间片耗尽,线程会被强制上下文切换,立刻让出CPU

  • 线程执行顺序、何时执行、执行多久,完全不可预测

  • 线程可以在任意两条机器指令之间被强行切走暂停

1.2 直接衍生的所有并发问题(100%核心来源)

问题1:竞态条件 → 破坏原子性(数据错乱)

业务操作大多是多指令组合(如count++:读→改→写三步),无锁状态下,线程可在步骤中间被切走:

  • 线程A读取数据、未写回 → 被切走暂停

  • 线程B抢占CPU,读取旧数据、执行修改并写回

  • 线程A恢复执行,覆盖B的结果,最终数据更新丢失

解决方案:synchronized、ReentrantLock、CAS,将多步操作封装为不可中断的临界区,保证原子性。

问题2:持有锁线程中途暂停 → 并发阻塞、性能暴跌

线程进入synchronized/AQS锁的临界区,执行一半代码,时间片耗尽被切走:

  • 锁不会释放(锁释放仅由代码逻辑控制,与CPU调度无关)

  • 所有竞争锁的线程全部阻塞排队

  • 持有锁的线程迟迟无法再次获取CPU,导致整个并发流程停滞

解决方案:临界区代码极简,禁止耗时IO、循环、睡眠操作。

问题3:唤醒/等待时序颠倒 → 信号丢失(wait/notify致命bug)

调度无序导致唤醒动作先执行,等待动作后执行的经典竞态时序:

  • 线程B先执行notify唤醒,但此时没有线程处于wait等待状态

  • notify是瞬时一次性信号,无等待线程则直接失效、信号丢失

  • 后续线程A执行wait,永久休眠卡死,无任何线程能唤醒它

解决方案

  • 底层AQS采用LockSupport.permit许可机制,预存唤醒凭证,解决时序颠倒问题

  • 业务层wait必须配合while循环二次校验条件

问题4:操作系统虚假唤醒 → 无理由唤醒等待线程

Linux futex内核机制特性:系统调度过程中,会无任何signal/notify信号,被动唤醒WaitSet、条件队列中的休眠线程。

线程唤醒后,业务条件并未满足,直接执行业务逻辑会引发严重bug。

解决方案禁止if判断等待条件,必须while循环循环校验,不满足条件继续等待。

问题5:队列线程唤醒不立即执行 → 并发滞后

notify/signal只会将线程从等待队列(WaitSet/条件队列)转移到锁竞争队列(EntryList/CLH队列),线程仅变为就绪状态:

  • 不会立即获取CPU执行

  • 最终执行顺序依旧由OS调度决定,无序竞争锁

核心结论唤醒 ≠ 拿到锁 ≠ 立即执行


二、硬件层根源:多核CPU缓存架构 → 可见性问题

2.1 硬件架构特性

现代CPU架构:寄存器 → L1/L2私有缓存(单核心独占) → L3共享缓存 → 主内存

为了极致性能,CPU不会每次读写都访问低速主内存,会将共享变量加载到核心私有缓存,每个物理核心拥有独立的数据副本。

2.2 直接衍生:缓存可见性问题

多个线程跑在不同CPU核心,操作同一个共享变量:

  • 线程A修改变量,仅更新自己的L1/L2私有缓存,未及时刷回主内存

  • 其他核心的线程B,读取本地私有缓存的旧数据

  • 多线程数据不一致,出现可见性丢失

2.3 底层辅助诱因

MESI缓存一致性协议为了性能优化,存在写缓冲区、失效队列,会延迟数据同步,进一步放大可见性问题。

2.4 解决方案

  • volatile:读写内存屏障,强制刷新缓存、读取最新主内存数据,保证可见性

  • synchronized/Lock:加锁前后强制刷新内存,天然保证可见性


三、软硬件优化根源:指令重排 → 有序性问题

3.1 重排来源(两层重排)

为提升执行效率,两处会主动打乱代码原有执行顺序:

  1. CPU硬件指令重排:CPU乱序执行指令,优化流水线吞吐

  2. JIT编译器编译重排:运行期优化代码指令顺序,不影响单线程结果

单线程下重排安全、无影响,但多线程+调度不确定性叠加后,会出现预期外的执行顺序。

3.2 经典bug案例

DCL双重检查锁单例模式:指令重排导致对象半初始化,其他线程获取未赋值完成的对象,引发空指针、数据异常。

3.3 解决方案

  • volatile:禁止跨内存屏障的指令重排,保住多线程有序性

  • Happens-Before规则:界定无需屏障也能保证有序的场景


四、三大根源汇总 → JMM三大特性缺失(并发问题总结)

所有并发异常,本质都是三层底层不确定性,导致JMM三大核心特性被破坏:

缺失特性

底层根源

现象

解决方案

原子性缺失

OS抢占调度,指令中途可被打断

多线程计数错乱、数据更新丢失

synchronized、Lock、CAS

可见性缺失

多核CPU私有缓存、数据同步延迟

一个线程修改,其他线程看不到最新值

volatile、锁机制

有序性缺失

CPU/JIT指令重排+调度无序

代码执行顺序错乱,出现诡异并发bug

volatile、Happens-Before


五、衍生高阶并发问题(全部溯源)

5.1 wait/notify 缺陷

根源:调度时序不确定 + 单等待队列

  • 信号丢失、无效唤醒、虚假唤醒

  • 无法精准唤醒,只能随机唤醒线程,产生惊群效应

5.2 AQS & Condition 设计意义

完全为了屏蔽底层不确定性而生:

  • LockSupport.permit:解决调度时序颠倒、信号丢失问题

  • 多条件队列:解决单队列无效唤醒问题,实现精准唤醒

  • CLH队列+自旋+park:适配OS调度,平衡性能与阻塞开销

5.3 锁升级设计(偏向→轻量→重量)

根源:线程竞争强度不确定、调度无序

JVM动态适配并发场景:无竞争偏向锁、轻微竞争轻量级锁(CAS自旋)、激烈竞争重量级锁(futex阻塞),最大化兼顾性能与并发安全。


六、终极闭环总结

1. 一切Java并发问题的终极根源:操作系统抢占式线程调度的不确定性(线程执行时序、切换时机完全不可控)。

2. 叠加多核CPU缓存机制,产生可见性问题;叠加软硬件指令重排,产生有序性问题。

3. 三层不确定性,彻底破坏了单线程代码的原子性、可见性、有序性,所有并发bug由此诞生。

4. 所有Java并发工具,没有一个是多余的:volatile、CAS、锁、AQS、LockSupport、Condition,全部都是为了对抗底层硬件、系统的不确定性,在混乱的并发调度中,人为构建安全、有序的多线程执行规则。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐