主题
界面
主题
进入 S6 Java 深度。并发只有两个根源问题:
① 可见性:一个线程的写入,另一个线程可能看不见 ✗
② 原子性:i++ 是三步操作,中间会被打断 ✗✗★ 本节实测(labs/java-deep 的 ConcurrencyLesson):
③ volatile 对照:立即退出 ✓✓★ Java 内存模型(JMM)规定:每个线程有自己的工作内存(主内存数据的副本)。 线程 A 改了变量 → 先写工作内存 → 何时刷回主内存不确定; 线程 B 读变量 → 从自己的工作内存读 → 可能读到旧值 ✗✗
★ 这不是理论 ——本节实验 4 就是它的可复现形式 ✓
子线程:while (!stop) { }
主线程:sleep 100ms → stop = true实测结果:
① 无 volatile:本次正常退出 ⚠️(JIT 未优化;多跑几次可能复现卡死)
② 有 volatile:立即退出 ✓✓(每次都如此)★★ 必须诚实说明:这个实验有时复现、有时不复现—— 复现时是 JIT 把 while(!stop) 优化成 if(!stop) while(true);(读一次就不再读)✗✗。 但 volatile 版本永远立刻退出 ——对照组的意义就在这里 ✓
★★★ 真正的教训:并发实验"结果不稳定" ≠ 随机运气,而是"没有同步边"的必然表现 ✓✓✓ 修法不是多跑几次碰运气,而是加上同步边(volatile/锁) ✓
| 规则 | 含义 |
|---|---|
| 程序顺序 | 单线程内,前面的操作对后面可见 |
| 锁 | 解锁 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):无边 → 可能看不见 ✓
private static volatile boolean running = true;
public void shutdown() { running = false; } // 立刻对所有线程可见 ✓var config = loadConfig(); // 主线程准备数据
var t = new Thread(() -> use(config));
t.start(); // start 前的写入对新线程可见 ✓✓
t.join(); // 线程的写入对主线程可见 ✓new Thread(() -> result = compute()).start();
Thread.sleep(1000);
use(result); // ✗✗ sleep 不是同步原语,result 可能还是 null代码:labs/java-deep/src/main/java/cn/kb/learn/deep/concurrency/ConcurrencyLesson.java
cd labs
.\mvnw.cmd -pl java-deep compile
java "-Dstdout.encoding=UTF-8" -cp java-deep/target/classes cn.kb.learn.deep.concurrency.ConcurrencyLesson实测输出(关键部分):
===== 4. 可见性 =====
① 无 volatile :本次正常退出 ⚠️(JIT 未优化,多跑几次可能复现)
② 有 volatile :退出耗时 <2 ms —— 立即退出 ✓✓★ 诚实记录:实验 4 的无 volatile 版本本次未复现卡死(JIT 未优化), 但 volatile 对照组永远立刻退出 ——这正是"对照组"的价值 ✓✓
System.out.println:JIT 不再优化 → 不卡了(体会"为什么不稳定")✓✓;| # | 错误 | 说明 |
|---|---|---|
| 1 | 以为写了共享变量对方就能看见 ✗✗ | 无同步边就看不见 ✓✓ |
| 2 | 用 sleep 代替同步 ✗✗ | sleep 不是同步原语 ✓ |
| 3 | 以为"没出事"就是没问题 ✗✗✗ | 没有同步边的代码是"定时炸弹" ✓✓ |
| 4 | long/double 变量不加 volatile ✗ | 64 位写可能被撕裂 ⚠️ |
| 5 | 停止标志不用 volatile ✗✗ | 本节实验的核心 ✓✓ |
验收方式:能用 happens-before 解释你写过的每段并发代码。
扩展任务:研究 双重检查锁单例: 为什么 instance 必须是 volatile?(提示:"new 对象"是"分配-初始化-赋值"三步,可能重排——另一个线程可能拿到"已分配但未初始化"的对象 ✗✗)★★
new Thread(() -> x = 1).start(); System.out.println(x); 一定打印 1 吗?《synchronized 与 volatile》:两种同步手段的分工与误区。