Skip to content

并发基础:线程、锁与内存模型 ​

S6-001建议学高级约 90 分钟JDK 21Java 高级与架构 · 并发编程

1. 本节定位 ​

进入 S6 Java 深度。并发只有两个根源问题:

text
① 可见性:一个线程的写入,另一个线程可能看不见 ✗
② 原子性:i++ 是三步操作,中间会被打断 ✗✗

★ 本节实测(labs/java-deep 的 ConcurrencyLesson):

text
③ volatile 对照:立即退出 ✓✓

2. 学习目标 ​

  • 能解释"为什么需要 volatile"(可见性)★★★;
  • 能背出 happens-before 六条规则并用于判断并发代码 ★★★;
  • 知道并发实验"结果不稳定"的原因与对策 ★★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

★ Java 内存模型(JMM)规定:每个线程有自己的工作内存(主内存数据的副本)。 线程 A 改了变量 → 先写工作内存 → 何时刷回主内存不确定; 线程 B 读变量 → 从自己的工作内存读 → 可能读到旧值 ✗✗

★ 这不是理论 ——本节实验 4 就是它的可复现形式 ✓

5. 核心概念 ​

5.1 可见性实验(结果受 JIT 影响,如实呈现)★★★ ​

text
子线程:while (!stop) { }
主线程:sleep 100ms → stop = true

实测结果:

text
① 无 volatile:本次正常退出 ⚠️(JIT 未优化;多跑几次可能复现卡死)
② 有 volatile:立即退出 ✓✓(每次都如此)

★★ 必须诚实说明:这个实验有时复现、有时不复现—— 复现时是 JIT 把 while(!stop) 优化成 if(!stop) while(true);(读一次就不再读)✗✗。 但 volatile 版本永远立刻退出 ——对照组的意义就在这里 ✓

★★★ 真正的教训:并发实验"结果不稳定" ≠ 随机运气,而是"没有同步边"的必然表现 ✓✓✓ 修法不是多跑几次碰运气,而是加上同步边(volatile/锁) ✓

5.2 happens-before 六条规则 ★★★ ​

规则含义
程序顺序单线程内,前面的操作对后面可见
锁解锁 happens-before 后续加锁(synchronized 有效的根源)
volatile写 volatile happens-before 后续读
线程启动start() 前的操作对新线程可见
线程终止线程的所有操作在 join() 返回后可见
传递性A hb B,B hb C → A hb C

★ 用法:判断并发代码对不对 = 找它有没有 happens-before 边: 实验 1(i++):无边 → 丢失更新 ✗;实验 2(synchronized):有锁边 → 正确 ✓; 实验 4(无 volatile):无边 → 可能看不见 ✓

5.3 三条工程结论 ★★★ ​

  1. 共享可变状态 = 麻烦的源头 ——能不共享就不共享(不可变 / ThreadLocal / 消息传递)✓✓
  2. 必须共享时:只读用 volatile/不可变,读改写用锁或原子类 ✓✓
  3. 测试不稳定不是运气问题 ⚠️ ——是没有同步边的必然表现 ✓✓

6. 递进示例 ​

6.1 最小示例:volatile 停止标志 ​

java
private static volatile boolean running = true;
public void shutdown() { running = false; }   // 立刻对所有线程可见 ✓

6.2 业务示例:start/join 的 happens-before ​

java
var config = loadConfig();                    // 主线程准备数据
var t = new Thread(() -> use(config));
t.start();                                    // start 前的写入对新线程可见 ✓✓
t.join();                                     // 线程的写入对主线程可见 ✓

6.3 反例:靠 sleep 传递数据 ​

java
new Thread(() -> result = compute()).start();
Thread.sleep(1000);
use(result);          // ✗✗ sleep 不是同步原语,result 可能还是 null

7. 完整可运行代码与实测 ​

代码:labs/java-deep/src/main/java/cn/kb/learn/deep/concurrency/ConcurrencyLesson.java

powershell
cd labs
.\mvnw.cmd -pl java-deep compile
java "-Dstdout.encoding=UTF-8" -cp java-deep/target/classes cn.kb.learn.deep.concurrency.ConcurrencyLesson

实测输出(关键部分):

text
===== 4. 可见性 =====
  ① 无 volatile   :本次正常退出 ⚠️(JIT 未优化,多跑几次可能复现)
  ② 有 volatile   :退出耗时 <2 ms —— 立即退出 ✓✓

★ 诚实记录:实验 4 的无 volatile 版本本次未复现卡死(JIT 未优化), 但 volatile 对照组永远立刻退出 ——这正是"对照组"的价值 ✓✓

8. Debug 观察点 ​

  1. 多跑几次实验:观察结果波动 ⚠️;
  2. 给空转循环加一行 System.out.println:JIT 不再优化 → 不卡了(体会"为什么不稳定")✓✓;
  3. 用 jconsole 看线程状态 ✓。

9. 常见错误 ​

#错误说明
1以为写了共享变量对方就能看见 ✗✗无同步边就看不见 ✓✓
2用 sleep 代替同步 ✗✗sleep 不是同步原语 ✓
3以为"没出事"就是没问题 ✗✗✗没有同步边的代码是"定时炸弹" ✓✓
4long/double 变量不加 volatile ✗64 位写可能被撕裂 ⚠️
5停止标志不用 volatile ✗✗本节实验的核心 ✓✓

10. 动手实验 ​

  1. 多跑几次实验,记录结果波动 ⚠️;
  2. 给空转循环加一行打印,观察 JIT 优化消失 ✓✓;
  3. 把停止标志改成 AtomicBoolean,对比 volatile(都可见,但原子类还能"读改写")✓;
  4. 写一个"没有同步边"的配置读取例子,用 happens-before 表判断 ✓。

验收方式:能用 happens-before 解释你写过的每段并发代码。

扩展任务:研究 双重检查锁单例: 为什么 instance 必须是 volatile?(提示:"new 对象"是"分配-初始化-赋值"三步,可能重排——另一个线程可能拿到"已分配但未初始化"的对象 ✗✗)★★

11. 练习题 ​

  1. 判断:volatile 修饰的 int 计数器多线程自增是安全的。(对 / 错)
  2. 填空:happens-before 的六条规则是 、、、、、。
  3. 简答:为什么并发测试"结果不稳定"反而是重要信号?
  4. (判断)new Thread(() -> x = 1).start(); System.out.println(x); 一定打印 1 吗?
  5. (设计)一个"健康检查标志"由后台线程每 5 秒更新,主线程频繁读——怎么声明?
参考答案(要点)
  1. 错 ✗✗。volatile 保证可见性与有序性,但 i++ 的读改写不原子 ✓✓(本节实测丢 197 次)★
  2. 程序顺序;锁;volatile;线程启动;线程终止;传递性 ✓
  3. 不稳定 = 没有同步边 ✗✓ ——它证明存在真实竞态,只是这次没踩中 ✓✓; "没出事"会让人误以为代码正确 ✗✗★
  4. 不一定 ✗ ——start() 有 happens-before 边(启动规则), 但主线程没有 join,读取与子线程写入之间无边 ✗✓★
  5. volatile boolean healthy ✓✓ ——只读场景,可见性足够(不需要锁)✓★

12. 掌握标准 ​

  • [ ] 能解释可见性问题与 volatile 的作用 ★★★;
  • [ ] 能背出 happens-before 六条规则 ★★★;
  • [ ] 能用 happens-before 判断并发代码 ★★★;
  • [ ] 知道并发实验不稳定的意义 ★★★。

13. 扩展资源 ​

14. 下一步学习 ​

《synchronized 与 volatile》:两种同步手段的分工与误区。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引