并发三大问题与 JMM 的使命

多线程共享变量时,Bug 通常来自三个方向:可见性、原子性、有序性。可见性指一个线程的修改另一个线程看不到;原子性指「读改写」被拆成多步,中间被插队;有序性指编译器与 CPU 为优化重排指令,执行顺序与源码不一致。

Java 内存模型(Java Memory Model,JMM)就是一份「并发契约」:它规定哪些读写组合是合法的、哪些重排被禁止、什么时候一个线程的写入对另一个线程可见。遵守这份契约写代码,结果才可预测;违反它,Bug 只在特定硬件与负载下偶现,极难复现。

public class Counter {
    private int value = 0;
    public void increment() { value++; }
    public int get() { return value; }
}

上面的 `value++` 不是原子操作:它要「读旧值、加一、写回」三步,两个线程并发调用时可能互相覆盖,结果比预期小。这不是锁没加对的问题,而是对内存模型理解不到位——并发安全不是「碰运气」,而是严格按契约组合同步原语。

JMM 是并发代码的「交通规则」:不看规则开车,事故只在路口偶发。
本文核心知识点
  • happens-before规则体系。
  • volatile可见性与禁重排。
  • synchronized锁升级过程。
  • CAS与原子类原理。
Java 内存模型配图:蓝调插画中多层堆叠芯片结构被透明环形栅栏圈层环绕,粒子光束穿过,体现主内存与线程工作内存的隔离协作
主内存像共享的中央仓库,每个线程又带着自己的工作内存。

主内存与工作内存模型

JMM 把内存抽象成两层:所有线程共享的「主内存」,以及每个线程私有的「工作内存」。线程读变量,先拷到自己的工作内存;写变量,也是先写工作内存再同步回主内存。同步时机不由程序员控制,这就是可见性问题的根源。

JMM 给出的核心保障是一组 happens-before 规则:如果操作 A happens-before 操作 B,那么 A 的结果对 B 可见,且 A 的指令不会被重排到 B 之后。常用规则包括:程序顺序规则、锁规则(解锁 happens-before 后续加锁)、volatile 规则、线程启动与终止规则、传递性规则。

// 锁规则:线程 A 解锁后,线程 B 获取同一把锁,
// 则 A 在临界区内的所有写入对 B 可见
synchronized (lock) {
    sharedValue = 100;
}

这些规则是推导并发正确性的地基:只要代码路径上存在 happens-before 链,数据就是安全的;找不到这样的链,就必须补同步原语,而不是指望「多跑几次没问题」。

volatile 语义与使用边界

`volatile` 保证两件事:写 volatile 变量立即刷回主内存,读 volatile 变量直接从主内存拿;同时对 volatile 变量的读写禁止重排。它解决可见性与有序性,但**不解决原子性**——`volatile int` 的自增依然不安全。

// 正确用法:状态开关,一写多读
volatile boolean running = true;

while (running) {
    doWork();
}
// 另一个线程 running = false 后,循环一定能看到

典型误用是把 volatile 用在计数器、余额这类「读改写」场景。正确边界是:该变量要么只被单线程写、多线程读;要么写操作本身就是原子的(如布尔开关、引用替换)。超过这个边界,就该上原子类或锁。

synchronized 与锁升级

`synchronized` 是互斥同步的基石:进入临界区前获取锁,退出时释放锁,同时建立 happens-before 关系。现代 JVM 对它做了大量优化:无竞争时用偏向锁,轻度竞争升级为轻量级锁(自旋),竞争激烈才膨胀为重量级锁(阻塞挂起)。

public synchronized void transfer(Account to, int amount) {
    this.balance -= amount;
    to.balance += amount;
}

锁的是对象不是方法:实例方法锁 this,静态方法锁 Class 对象。锁粒度决定并发度,能用「更小的锁对象」或「更短的临界区」就不要锁整类。锁升级让 synchronized 在低竞争下性能不输显式锁,写起来还更简单,是现代 Java 并发代码的首选互斥工具。

CAS 与原子类

原子类不走锁,走 CAS(Compare-And-Swap):比较当前值与期望值,相等才更新,否则重试。它无锁、无阻塞、轻量,适合计数、状态位这类高频小操作。`AtomicInteger`、`AtomicLong`、`AtomicReference` 都是典型代表。

AtomicLong total = new AtomicLong(0);
long prev = total.getAndAdd(100);
// 高并发下替代 synchronized 计数,吞吐更高

CAS 有个经典陷阱叫 ABA 问题:值从 A 变成 B 又变回 A,CAS 会误判「没被改过」。业务不在意中间过程时无所谓,在意版本变化就用 `AtomicStampedReference` 带版本号。超高竞争的计数场景,`LongAdder` 把竞争分散到多个单元,读时才汇总,比 AtomicLong 更激进。