主内存 ↔ 工作内存的抽象: 三大性质 (原子/可见/有序) · happens-before 规则 · volatile 的能与不能
JMM 是一份"契约": 它不描述硬件, 只承诺在哪些前提下, 一个线程的写保证对另一个线程可见 — 这个前提就是 happens-before。volatile/锁/线程 start·join 都是制造 HB 边的手段。没有 HB 边时, 你观察到的"一切正常"只是编译器与缓存恰好没动手 — 换个 CPU 或 JDK 版本就可能翻车。
a = 1; r1 = b; // 线程 A b = 1; r2 = a; // 线程 B → r1==0 && r2==0 可能出现 // 关键: 乱序+缓存让"代码顺序"≠"可见顺序", JMM 划清界限
volatile boolean stop = false; // 关键: 写 ≈ 普通写 + StoreLoad 屏障(x86: lock addl) // 比锁便宜, 但每次读写都有屏障成本 — 不是免费的
count++ 是读-改-写三步, volatile 只保证每步读写都"新鲜", 两线程仍可同时读到相同值再各自+1。计数请用 AtomicLong/LongAdder。 volatile int count = 0; count++; // 读-改-写三步: 两线程同读 5 → 各+1 → 都写 6 → 丢一次 // 关键: volatile 只保鲜不原子 → AtomicLong / LongAdder
LongAdder n = new LongAdder(); n.increment(); // 写: 分散到 Cell, 高竞争几乎无自旋 n.sum(); // → 读: 弱一致瞬时和, 监控够用
// CAS: "值是 A 才换成 B" — A→B→A 的中间变化被漏判 AtomicStampedReference<Node> ref; // 版本号解 ABA ref.compareAndSet(a, b, s1, s2); // 值 + 版本双判
volatile long seq; // 对: volatile 保证 64 位读写原子 // 错(示意): 裸 long 依赖平台 — 罕见架构可撕裂成两个 32 位
final class Point { final int x, y; } // 关键: 不逃逸 this 的 final 字段, 他线程必见构造后的值 // → 不可变对象天然线程安全的法律基础
Quartz/轮询任务收到 SIGTERM 后停止拉新任务, 跑完手头的再退出 — 单写多读的教科书场景:
public class Worker { private volatile boolean running = true; // 立即可见于所有线程 public void run() { while (running) { // 无 volatile: JIT 可能把 running 缓存进寄存器 → 死循环 processOneJob(); } drainAndFlush(); } public void shutdown() { running = false; } // shutdown hook / 信号处理器调用 }
public class Router { private static volatile Router instance; // 少了 volatile = 半成品风险 public static Router get() { if (instance == null) { // 一读: 无锁快路径 synchronized (Router.class) { if (instance == null) // 二读: 防重复创建 instance = new Router(); // 分配→初始化→赋值可能重排! } } return instance; } } // 无 volatile: 线程B 可能看到非空但未初始化完的 instance → NPE 玄学
public final class Config { // 不可变: 所有字段 final private final Map<String, Integer> limits; private Config(Map<String, Integer> l) { this.limits = Map.copyOf(l); } } private volatile Config cfg = load(); void reload() { cfg = new Config(parseFromApollo()); } // 整体替换, 读侧永远拿到一致一代 // 套路与 Go atomic.Pointer 快照相同: 写时整体换, 读时无锁拿快照
千万级 QPS 的计数, AtomicLong 自旋浪费, LongAdder 分散热点:
public final class QpsCounter { private final LongAdder count = new LongAdder(); public void hit() { count.increment(); } // 写路径: 分散到 Cell, 几乎无竞争 public long snapshot() { return count.sum(); } // 读路径: 弱一致求和, 监控够用 } # 读多写少要精确一致时仍是 AtomicLong; 监控计数一律 LongAdder
只有一两个字段需要原子更新时, 不必把整对象包 AtomicReference:
private static final AtomicReferenceFieldUpdater<Router, Rules> RULES = AtomicReferenceFieldUpdater.newUpdater(Router.class, Rules.class, "rules"); private volatile Rules rules; // 字段本身必须 volatile public void reload(Rules next) { RULES.set(this, next); } // 整体换快照 public Route match(String path) { return rules.find(path); } // 无锁读
put/take 自带 happens-before — 生产者放入前的所有写, 对拿到它的消费者可见, 常被忽略的白送保证:
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(1000); # 生产者: 构建完整个对象图(含内部 list/dict)才 put → 消费者 take 后完整可见 queue.put(newTask(ids, context)); # 消费者无需再加锁/volatile: take() 返回即已建立 HB 边 Task t = queue.take(); t.execute();
volatile 停止位 + CountDownLatch 等清理完成, 两阶段各司其职:
private volatile boolean running = true; private final CountDownLatch done = new CountDownLatch(4); public void worker() { try { while (running) process(); } finally { flush(); done.countDown(); } // 退出前必收尾 } public void shutdown() throws InterruptedException { running = false; // 阶段1: 广播停 if (!done.await(15, TimeUnit.SECONDS)) // 阶段2: 等收尾, 超时强杀 log.warn("graceful timeout, some workers may not flush"); }
"没有就创建"的检查-放入必须用原子方法, 手写 containsKey+put 必丢:
ConcurrentHashMap<String, List<Metric>> groups = new ConcurrentHashMap<>(); # 好: 原子复合 — 同 key 并发只有一个 lambda 生效 groups.computeIfAbsent(key, k -> new CopyOnWriteArrayList<>()).add(m); # merge 累加同款: counts.merge(id, 1L, Long::sum)
"偶尔重现"的并发问题, 用 OpenJDK 的并发压测框架变成确定性结论:
@JCStressTest @Outcome(id = "1, 1", expect = ACCEPTABLE, desc = "串行或正确同步") @Outcome( expect = FORBIDDEN, desc = "可见性丢失") public static class VisibilityRace { int x; // 非 volatile: 压测能打出 FORBIDDEN 结果即实锤 @Actor void writer(I r) { x = 1; r.r1 = 1; } @Actor void reader(I r) { r.r2 = x; } }
final 字段的构造保证 + volatile 引用 = 无锁共享的黄金组合:
public final class RateTable { // final 类 + 全 final 字段 private final double[] rates; RateTable(double[] src) { this.rates = src.clone(); } // 防御性拷贝 public double get(int i) { return rates[i]; } // 只读, 天生线程安全 } private volatile RateTable table = load(); // volatile 引用整体替换 # 读侧零锁零拷贝, 写侧原子换表 — 配置/汇率/路由的万能形态
volatile int count; count++ 照样丢更新。正解: AtomicLong(要精确) / LongAdder(高并发监控)。 volatile int count; count++; // 错: 读-改-写照样丢更新 LongAdder count = new LongAdder(); count.increment(); // 对
private static Router instance; // 错: 重排→半成品 NPE private static volatile Router instance; // 对: 或改静态内部类
if (!ready) wait() 式检查一次就睡 = 错过唤醒。正解: while 循环重检(防虚假唤醒), 配 wait/notify 或 Condition/BlockingQueue。 if (!ready) lock.wait(); // 错: 只查一次, 错过唤醒 while (!ready) lock.wait(); // 对: 循环重检防虚假唤醒
volatile int min, max; min=a; max=b; // 错: 两写之间可被观察 stats = new Stats(a, b); // 对: 不可变整体替换
System.out.println(x); // 错: 内部 synchronized 建 HB 边, 掩盖竞态 // 对: 删掉 print 后仍正确, 才算真的正确
// 错: 字段一律标 volatile "求保险" — 屏障成本+复合状态照样错 // 对: 先圈定共享边界, 只在边界上加同步
synchronized (lock) { x = 1; } // 解锁即刷回, 可见性白送 // 对: 同一把锁保护的字段不必再 volatile(冗余)
volatile int[] a 只保证引用可见, a[0]=1 的写对其他线程无同步保证。正解: AtomicIntegerArray / AtomicLongArray, 或 volatile 引用整体替换新数组。 volatile int[] a; a[0] = 1; // 错: 只保证引用可见 AtomicIntegerArray a; a.set(0, 1); // 对: 元素级原子
count += n 是读-加-写三步, volatile/普通字段都不原子。正解: Adder/AtomicLong; 或锁。 count += n; // 错: 读-加-写三步, 并发会丢 counter.add(n); // 对: AtomicLong.add / LongAdder.add
Map<String,Object> m = new HashMap<>(); // 错: 并发写扩容出事 Map<String,Object> m = new ConcurrentHashMap<>(); // 对
static final SimpleDateFormat F; F.parse(s); // 错: 错乱日期 DateTimeFormatter.ofPattern("yyyy-MM-dd"); // 对: 不可变
static final Random R = new Random(); R.nextInt(); // 错: 种子竞争 ThreadLocalRandom.current().nextInt(); // 对: 实例级隔离
synchronized(lockRef) 期间别处 lockRef = new Object() → 后续线程锁的是新对象, 互斥失效。正解: 锁对象 final; 字段更新型同步用 AtomicReference 状态机。 Object lock; lock = new Object(); // 错: 换锁后互斥失效 final Object lock = new Object(); // 对: 锁对象不可变
globalMap.put(k, mutableCfg); // 错: 可能半初始化 globalMap.put(k, ImmutableCfg.of(...)); // 对: 构造完再发布
while (!ready) lock.wait(); 永远 while。 if (!ready) monitor.wait(); // 错: 虚假唤醒只查一次 while (!ready) monitor.wait(); // 对: 永远 while
lock.notify(); // 错: 可能叫醒等另一条件的线程 lock.notifyAll(); // 对: 保守; 或多 Condition 精准唤醒
t.start(); t.start(); // 错: → IllegalThreadStateException pool.submit(task); // 对: 重跑建新线程或用池
synchronized(m) { cond.await(); } // 错: IllegalMonitorStateException lock.lock(); try { cond.await(); } ... // 对: 一锁一套等待体系
// 错: "都加了 volatile = 全局有序" — 无边的两线程顺序任意 // 对: 画共享数据依赖图, 每条边都要有 HB 手段
# 错: x86 强内存模型上"看起来对"就发布 # 对: jcstress 压测验证; 切 ARM(graviton)必跑并发回归