JVM到JUC过度:JMM
原子性
定义: 指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
通过一个例子回顾一下:有一个初始值为 0 的静态变量,一个线程对其自增,一个线程对其自减,进行 5000 次,最终结果会是 0 吗?
结果不一定是 0。
1 | static int i = 0; |
分析
对于 i++ 而言(i 是静态变量),实际会产生如下的 JVM 字节码指令:
1 | getstatic i // 获取静态变量 i 的值 |
对应 i– 也是类似:
1 | getstatic i // 获取静态变量 i 的值 |
Java 的内存模型如下,完成静态变量的自增、自减需要在主内存和线程内存(工作内存)中进行数据交换:

在多线程的环境下,这 8 行指令可能会交错执行,导致最终结果错误。
出现负数的情况:
1 | // 假设 i 的初始值是 0 |
解决方案
使用synchronized
1 | synchronized ( 对象 ) { |
1 | static int i = 0; |
可见性
在下列代码中,main 线程对 run 变量的修改对于 t 线程不可见,导致 t 线程无法停止:
1 | static boolean run = true; |
分析
初始状态下,t 线程刚开始从主内存中读取了 run 的值到工作内存中:

因为 t 线程需要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存到自己工作内存中的高速缓存中,减少对主内存中 run 的访问,以提高效率:

1 秒后,main 线程修改了 run 的值,并同步至主内存,而 t 线程还是从自己工作内存中的高速缓存中读取这个变量的,结果永远是旧值:

解决方案
A. volatile 关键字(最轻量级)
它是可见性的代名词。一旦一个变量被声明为 volatile,JMM 会强制执行两件事:
- 立刻刷回:一旦线程修改了该变量,必须立即刷回主内存。
- 失效通知:其他线程工作内存里的该变量副本强制失效,必须重新去主内存读取。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 static volatile boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (run) {
// ...
}
});
t.start();
Thread.sleep(1000);
run = false;
}
B. synchronized 关键字
它的可见性保证更强,规则如下:
- 加锁前:清空工作内存,从主内存重新读取。
- 解锁前:必须把工作内存的值刷回主内存。
如果不使用 volatile 关键字,而是在死循环内部加入 System.out.println() 方法,会发现线程 t 也能正常停止,这是因为在 println() 方法内部使用了 synchronized 关键字,保证了主内存和工作内存之间的数据一致。
有序性
线程 A 的代码(生产者):
Java
1 | a = 1; // 步骤 1:准备数据 |
线程 B 的代码(消费者):
Java
1 | if (flag) { // 步骤 3:检查标志位 |
在正常逻辑下,我们预期输出一定是 1。但如果没有 volatile 保证有序性,发生了重排序:
分析
线程 A 内部发生了重排:
由于 a 和 flag 没有直接的数据依赖(改 a 不影响改 flag),CPU 为了优化,可能会先执行 flag = true。
- 时刻 T1:线程 A 执行了
flag = true(步骤 2 提前了)。此时a还是0。 - 时刻 T2:线程 B 进来执行,发现
flag已经是true了。 - 时刻 T3:线程 B 执行
System.out.println(a),结果输出了0! - 时刻 T4:线程 A 这才执行
a = 1(步骤 1 滞后了)。
结论:线程 B 拿到了一个“逻辑上还没准备好”的脏数据。这就是重排序导致的逻辑断层。
解决方案
为了禁止这种“乱排”行为,Java 使用了内存屏障。当你给变量加上 volatile 时,汇编底层会插入特殊的指令:
- LoadStore 屏障:保证前面的读操作在后面的写操作之前完成。
- StoreStore 屏障:保证前面的写操作在后面的写操作之前对其他线程可见。
在上面的例子中: 如果 flag 加了 volatile,线程 A 的代码就会被强制执行:
a = 1- [StoreStore 屏障] —— 像一道墙,规定前面的写操作必须先完成。
flag = true
这样线程 B 只要看到 flag 为 true,就一定能确定 a 已经是 1 了。
应用
DCL 单例模式
1 | public class Singleton { |
第一次检查 (if 外面): 如果 instance 已经创建好了,直接返回即可,不需要进入锁环节。这极大提高了高并发下的性能(毕竟 synchronized 是有开销的)。
加锁 (synchronized): 确保同一时刻只有一个线程能执行创建对象的逻辑。
第二次检查 (if 里面): 这是为了拦截那些“第一批冲进来的线程”。
场景模拟: 线程 A 和 B 同时发现
instance == null,A 先拿到了锁,B 在锁外面排队。A 创建完对象释放锁后,B 拿到锁进来了。如果没有第二次检查,B 会再new一个对象,单例模式就彻底破功了。
如果没有 volatile,instance = new Singleton(); 这一行会被拆成三步:
- 分配内存空间(给对象找个地儿)。
- 初始化对象(执行构造方法,填入数据)。
- 将 instance 指向分配的内存地址(此时 instance 不再是 null)。
由于指令重排序,步骤 2 和 3 可能会反过来:
- 分配空间。
- 将 instance 指向内存地址(此时对象还没初始化!)。
- 执行构造方法。
后果: 线程 A 刚跑完第 2 步,线程 B 刚好执行到第一次检查 if (instance == null)。发现 instance 不为 null,于是高高兴兴地把这个还没初始化完的“半成品”对象拿去用了,直接报空指针异常(NPE)。
两个关键字的对比
| 特性 | volatile | synchronized |
|---|---|---|
| 类型 | 变量修饰符(仅用于变量) | 关键字(可修饰方法、代码块) |
| 原子性 | 不保证(如无法解决 i++) |
保证(一次仅一线程执行) |
| 可见性 | 保证(强制刷回主存/失效缓存) | 保证(解锁前刷回主存) |
| 有序性 | 保证(通过内存屏障禁止重排) | 保证(通过单线程执行保证) |
| 是否阻塞 | 否(轻量级,无线程切换) | 是(可能导致线程阻塞和唤醒) |
| 性能 | 极高(接近普通变量读写) | 相对较低(涉及锁竞争和升级 |
剩下的JUC再学
完结撒花!