1. 引言
并发编程是 Java 开发者进阶的必经之路,而理解 Java 内存模型(Java Memory Model,JMM)则是掌握并发安全的基石。很多初学者在编写多线程程序时,会遇到「明明改了变量,另一个线程却看不到」「程序运行结果不稳定」等诡异问题,这些往往都与 JMM 的三大特性——可见性、原子性、有序性——有关。
本文将从 JMM 的核心概念出发,依次讲解 volatile 能解决什么、不能解决什么,synchronized 的锁升级机制,以及 happens-before 规则,最后通过实战案例帮助你把这些知识串起来。
2. 从 JMM 说起:可见性、原子性、有序性
Java 内存模型规定了多线程环境下共享变量的读写规则,其核心是解决三个问题:可见性、原子性、有序性。
2.1 可见性(Visibility)
可见性指的是:当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。
在 JMM 中,每个线程都有自己的工作内存(可以理解为 CPU 缓存),线程对变量的读写都发生在工作内存中,再同步回主内存。如果缺少同步机制,一个线程的修改可能不会及时刷新到主内存,其他线程读到的就是旧值。
public class VisibilityDemo {
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Thread reader = new Thread(() -> {
while (!flag) {
// 空转等待
}
System.out.println("flag 变为 true,退出循环");
});
reader.start();
Thread.sleep(100);
flag = true; // 主线程修改 flag
}
}
上面的代码中,reader 线程可能永远无法退出循环,因为主线程对 flag 的修改对 reader 线程不可见。
2.2 原子性(Atomicity)
原子性指的是:一个操作要么全部执行成功,要么全部不执行,中间不能被其他线程打断。
Java 中,对基本类型的赋值和读取(如 int x = 1)是原子的,但 i++ 这种「读-改-写」复合操作并不是原子的,它实际上包含三步:读取 i、计算 i+1、写回 i。在多线程环境下,这会导致结果丢失更新。
public class AtomicityDemo {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
count++;
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("count = " + count); // 结果通常小于 100000
}
}
2.3 有序性(Ordering)
有序性指的是:程序执行的顺序按照代码的先后顺序执行。但为了提升性能,编译器和 CPU 可能会对指令进行重排序(reordering),只要重排序后的结果与单线程执行结果一致即可。在多线程环境下,重排序可能导致意想不到的问题。
public class OrderingDemo {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100000; i++) {
x = 0; y = 0; a = 0; b = 0;
Thread t1 = new Thread(() -> { a = 1; x = b; });
Thread t2 = new Thread(() -> { b = 1; y = a; });
t1.start(); t2.start();
t1.join(); t2.join();
if (x == 0 && y == 0) {
System.out.println("第 " + i + " 次出现重排序");
}
}
}
}
3. volatile:能解决什么,不能解决什么
volatile 是 Java 提供的最轻量的同步机制,它主要解决可见性和有序性问题。
3.1 volatile 能解决什么
保证可见性:volatile 变量的写操作会立即刷新到主内存,读操作会从主内存读取,从而保证其他线程能看到最新值。
保证有序性:volatile 通过内存屏障禁止指令重排序,保证 volatile 变量读写前后的指令顺序。
public class VolatileDemo {
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Thread reader = new Thread(() -> {
while (!flag) {
// 空转等待
}
System.out.println("flag 变为 true,退出循环");
});
reader.start();
Thread.sleep(100);
flag = true; // volatile 保证 reader 线程能看到
}
}
3.2 volatile 不能解决什么
不能保证原子性:volatile 无法解决 i++ 这类复合操作的原子性问题,因为「读-改-写」三步之间仍可能被其他线程打断。
public class VolatileNotAtomicDemo {
private static volatile int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
count++;
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("count = " + count); // 仍然小于 100000
}
}
适用场景总结:
- 适合:状态标志位、单次发布不可变对象、作为「轻量级锁」使用。
- 不适合:需要保证原子性的复合操作、多个变量之间的约束关系。
4. synchronized:锁升级与原理
synchronized 是 Java 内置的同步机制,既能保证原子性,也能保证可见性和有序性。它的底层实现经历了从重量级锁到轻量级锁的演进,这就是所谓的「锁升级」。
4.1 synchronized 的基本用法
public class SynchronizedDemo {
private int count = 0;
// 同步方法
public synchronized void increment() {
count++;
}
// 同步代码块
public void incrementBlock() {
synchronized (this) {
count++;
}
}
}
4.2 锁升级过程
Java 6 之后,synchronized 引入了偏向锁、轻量级锁、重量级锁的升级机制,目的是在竞争不激烈时减少锁开销。
- 偏向锁:只有一个线程访问同步块时,锁会偏向该线程,减少不必要的 CAS 操作。
- 轻量级锁:当有第二个线程竞争时,偏向锁升级为轻量级锁,通过自旋等待获取锁。
- 重量级锁:当自旋超过阈值或竞争激烈时,升级为重量级锁,依赖操作系统互斥量,线程会阻塞。
4.3 synchronized 与 volatile 的对比
| 特性 | volatile | synchronized |
|---|---|---|
| 原子性 | 不保证 | 保证 |
| 可见性 | 保证 | 保证 |
| 有序性 | 保证(禁止重排序) | 保证 |
| 性能开销 | 较小 | 较大(有锁升级优化) |
| 适用场景 | 状态标志、单次发布 | 复合操作、临界区 |
5. happens-before 规则与实战案例
happens-before 是 JMM 定义的一组规则,用来判断两个操作之间是否存在「先行发生」关系。如果 A happens-before B,那么 A 的执行结果对 B 可见,且 A 的指令顺序在 B 之前。
5.1 核心 happens-before 规则
- 程序顺序规则:同一个线程中,前面的操作 happens-before 后面的操作。
- 监视器锁规则:对一个锁的解锁 happens-before 后续对这个锁的加锁。
- volatile 变量规则:对一个 volatile 变量的写 happens-before 后续对这个变量的读。
- 传递性规则:如果 A happens-before B,B happens-before C,则 A happens-before C。
5.2 实战案例:双重检查锁单例
经典的线程安全单例模式,结合 volatile 和 synchronized:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
这里 volatile 的作用是防止 new Singleton() 的指令重排序。new 操作实际上包含三步:分配内存、初始化对象、将引用指向内存。如果不加 volatile,编译器可能将后两步重排序,导致其他线程拿到一个未初始化完成的对象。
5.3 实战案例:生产者-消费者模型
利用 volatile 作为状态标志,配合 synchronized 保证临界区安全:
public class ProducerConsumerDemo {
private static volatile boolean running = true;
private static final Object lock = new Object();
private static int buffer = 0;
public static void main(String[] args) throws InterruptedException {
Thread producer = new Thread(() -> {
while (running) {
synchronized (lock) {
buffer++;
System.out.println("生产,buffer = " + buffer);
}
try { Thread.sleep(50); } catch (InterruptedException e) { }
}
});
Thread consumer = new Thread(() -> {
while (running) {
synchronized (lock) {
if (buffer > 0) {
buffer--;
System.out.println("消费,buffer = " + buffer);
}
}
try { Thread.sleep(80); } catch (InterruptedException e) { }
}
});
producer.start();
consumer.start();
Thread.sleep(1000);
running = false; // volatile 保证两个线程都能看到停止信号
producer.join();
consumer.join();
}
}
6. 总结
本文从 JMM 的三大特性出发,梳理了并发编程的核心知识:
- 可见性、原子性、有序性是并发安全的三个维度,理解它们是排查并发问题的前提。
- volatile 解决可见性和有序性,但不解决原子性,适合状态标志等轻量场景。
- synchronized 通过锁升级机制在保证原子性的同时兼顾性能,是临界区保护的默认选择。
- happens-before 规则是判断操作间可见性的理论依据,双重检查锁单例是它的经典应用。
在实际开发中,建议优先使用 java.util.concurrent 包下的工具类(如 AtomicInteger、ConcurrentHashMap),它们封装了底层的并发细节,更不容易出错。理解 JMM 的底层原理,能帮助你在遇到疑难并发问题时快速定位根因。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/vipxieliang/article/details/166945457



