1151 字
约 3 分钟
6
S05-B01 · 并发三大特性与 JMM
2024-12-31
2026-11-07

适用读者:写过多线程但被"可见性/原子性/有序性"绕晕的人。本系列第一篇。
前置:S04 ✅、S02-B02(进程与线程)。
实测环境:OpenJDK 21.0.12.1 / 8 线程 × 20 万次(2026-11-05)。
覆盖旧文:A-1553(211) A-1119(214) A-1821(245) + 1594(164) 947(80) 937(72) 936(63) 1613(102) 944(74) = 769 点。
覆盖扩展:E-S05-001(happens-before)、E-S05-002(CAS 与 ABA)。
下一篇:S05-B02 · 线程状态机。


1. 一句话结论

并发 bug 只有三类根因:看不到(可见性)、抢不到(原子性)、排错序(有序性)。JMM 就是规定"什么时候能看到"的那套规则。

特性 出问题的表现 解决手段
可见性 一个线程改了,另一个线程看不到 volatile、synchronized、final
原子性 i++ 丢更新 synchronized、Atomic*
有序性 指令重排导致"半初始化对象" volatile(禁止重排)、happens-before

2. 实测:i++ 到底丢多少

// 8 线程,每个线程 20 万次 plain++
static int plain = 0;
for (int j = 0; j < 200000; j++) plain++;

实测输出:

1. 无锁 plain++ 期望 1600000 实际 1009491(丢失 590509)耗时 121 ms
2. synchronized 结果 1600000 耗时 101 ms
3. AtomicInteger 结果 1600000 耗时 79 ms
方式 结果 耗时 说明
无锁 plain++ 丢失 37% 121 ms i++ = 读-改-写三步,非原子
synchronized 正确 101 ms 互斥(JDK 21 上乐观锁/轻量级优化后不慢)
AtomicInteger 正确 79 ms CAS 自旋,无锁

三个结论:

  1. i++ 不是原子操作——这是并发丢数据的第一大来源。
  2. synchronized 在低竞争下并不慢(实测甚至比无锁快,因为无锁版本有缓存行乒乓)。
  3. Atomic* 在计数场景最优。

3. volatile:只保证可见性 + 有序性,不保证原子性(E-S05-001)

保证 volatile synchronized Atomic
可见性 ✅ ✅ ✅
有序性 ✅(禁止重排) ✅ ✅
原子性 ❌(i++ 仍会丢) ✅ ✅

判据:"这个变量只被单线程写、多线程读?" 是 → volatile 够用(如状态标志);否则要锁或原子类。

3.1 happens-before:JMM 的核心规则

规则 含义
程序顺序 同一线程内,前面的语句先行于后面的
监视器锁 解锁先行于后续加锁
volatile 对 volatile 的写先行于后续读
线程启动/终止 start() 先行于线程内操作;线程内操作先行于 join() 返回
传递性 A→B、B→C ⇒ A→C

这条规则解释"为什么 synchronized 能保证可见性":不是靠"内存立刻同步",是靠 happens-before 约束。


4. CAS 与 ABA(E-S05-002)

// CAS:比较并交换(CPU 指令 cmpxchg)
if (value == expect) { value = newValue; return true; }
问题 说明 解法
ABA 值从 A→B→A,CAS 以为没变 加版本号(AtomicStampedReference)
自旋开销 高竞争下空转 CPU 退避、或用锁
只能保证一个变量 多变量需 AtomicReference 或锁 —

实测第 3 行(AtomicInteger 79 ms vs synchronized 101 ms) 说明:低竞争下 CAS 明显更划算;高竞争下自旋会反噬。


5. 五个误区

误区 真相
"volatile 能替代锁" 不保证原子性
"synchronized 一定慢" 低竞争下很快(有锁消除/偏向优化)
"i++ 是原子的" 读-改-写三步
"内存可见性问题靠 sleep 能解决" 只是碰巧,不是保证
"代码顺序就是执行顺序" 编译期/CPU 都可能重排(受 happens-before 约束)

6. 自检

  1. 三大特性分别对应什么 bug?各用什么手段解决?
  2. 实测里 plain++ 丢了多少?为什么?
  3. volatile 保证什么、不保证什么?
  4. happens-before 的五条规则里,哪条解释了 synchronized 的可见性?
  5. ABA 问题怎么产生的?怎么解决?

7. 下一篇

S05-B02 · 线程状态机与创建方式

下一篇讲线程的生命周期:6 种状态怎么流转、BLOCKED 与 WAITING 的区别、以及 start() 与 run() 的本质差异。


本篇属于系列《S05 · Java 并发与 JVM》。旧文来源:A-1553/1119/1821/1594/947/937/936/1613/944;扩展点 E-S05-001/002。竞态实测为 OpenJDK 21(8 线程 × 20 万次)。

S05-B01 · 并发三大特性与 JMM
https://www.clxhxhhr.top/posts/5242/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。