深入理解Java高并发编程(4) - Java内存模型

1. JMM介绍

JMM是Java内存模型,定义了主存(线程共享的内存),工作内存(线程私有的变量)抽象概念,底层对应着CPU寄存器,缓存,硬件内存,CPU指令优化等。

JMM体现在以下几方面:

  • 原子性:保证指令不会受到线程上下文切换影响
  • 可见性:保证指令不会受cpu缓存的影响
  • 有序性:保证指令不会收cpu指令并行优化的影响

2. 可见性问题

JIT编译器会把多次从主存中读取的值缓存到了线程私有的工作内存中,当另一个线程对主存中的变量进行修改,该线程仍然读的是工作内存中的数据而产生的问题,这就是可见性问题。

3. 有序性问题

3.1 什么是指令重排序?

JVM会对指令的执行顺序进行重排,在底层的cpu有多种执行单元,不同的执行单元执行不同的事情,比如取指令,指令译码,执行指令,内存访问,数据写回等步骤,如果cpu还按照这个顺序一条指令一条指令的执行,有些执行单元就会闲置,当指令重排之后,就能保证cpu执行这条指令的取指令时还能执行另一条指令的指令译码。

比如同时支持取指令,指令译码,执行指令,内存访问,数据写回的处理器就被称为五级指令流水线。

image-20260311115249956
当然指令也不是随便重排的,重排指令不能影响结果。

3.2 指令重排序带来的问题

单线程下指令重排序是没有问题的,在多线程场景下指令重排序可能会带来一些问题。

在多线程场景下,指令重排可能会导致一个线程的行为对另一个线程来说难以预测。打比方说线程1修改了一个共享变量,线程2想输出这个共享变量,指令重排就可能导致线程2在修改前输出这个共享变量,也可能在修改后输出共享变量。

4. synchronize对于原子性,可见性,有序性

  • 原子性:
    • synchronized通过给对象加锁,保证临界区代码只有一个线程能访问从而保证了原子性
  • 可见性:
    • 锁释放(Unlock)时: 线程会强制将工作内存(CPU缓存)中修改的共享变量刷新回主内存
    • 锁获取(Lock)时: 线程会强制将工作内存中的共享变量置为无效,这样后续使用该变量时,就必须从主内存中重新加载最新的值。
  • 有序性:synchronized 的有序性保证和 volatile 有些类似,也是通过内存屏障来实现的。
    • 加锁(MonitorEnter):会插入 LoadLoad + LoadStore 屏障,屏障之后的所有读写操作,不能重排序到屏障之前
    • 释放锁(MonitorExit):会插入 StoreStore + StoreLoad 屏障,屏障之前的所有读写操作,不能重排序到屏障之后
内存屏障 结构示意 核心规则(禁止重排) 通俗理解
LoadLoad Load1 → LoadLoad → Load2 后面的读,不能重排到前面的读之前 先读前面,再读后面,读读有序
StoreStore Store1 → StoreStore → Store2 前面的写,不能重排到后面的写之后 先写前面,再写后面,写写有序
LoadStore Load → LoadStore → Store 后面的写,不能重排到前面的读之前 先读完,再写入,读写有序
StoreLoad Store → StoreLoad → Load 前面的写全部刷主存后,后面的读才能执行 写完刷主存,再开始读,写读有序

5. Volatile关键字

volatile关键字修饰的变量要求线程不能从工作缓存中读取变量,而是每次都从主存中去读取变量,线程操作Volatile变量都是直接操作主存,这样虽然有效率损失,但是能保证可见性。

Volatile可以保证可见性和有序性,但是不能保证原子性,因为他仅仅是操作了主存,没有保证一系列的指令是原子的

5.1 Volatile 如何保证可见性和有序性

Volatile通过读写屏障来保证可见性和有序性;

  • 对volatile变量的赋值(写操作)会产生写屏障 LoadStore + StoreStore

  • 对volatile变量的读取(读操作)会产生读屏障 LoadLoad + LoadStore

  • 可见性:

    • 写屏障会保证在写屏障之前的所有对变量的改动,都同步到主存当中。
    • 读屏障保证在读屏障之后的所有对共享变量的读取,都会从主存中读取。
  • 有序性

    • 写屏障会保证写屏障之前的指令不会被指令重排到写屏障之后去,但是写屏障之前的代码还是会指令重排的。
    • 读屏障保证读屏障之后的指令不会被指令重排到读屏障之前去,但是在读屏障之后的代码还是会指令重排的。

6. DCL问题(double-checked locking)

6.1 懒汉式单例模式

对于懒汉式单例模式,单例一开始不会被初始化,直到调用getInstance()时,才会开始创建实例

6.2 DCL解决多线程下单例创建

懒汉式单例创建时属于共享变量,存在线程安全问题。

采用DCL的机制可以避免单例被多次创建

public final class Singleton {
	private Singleton() {}
	private static volatile Singleton INSTANCE = null;
	public static Singleton getInstance() {
        //第一次操作共享变量也要保证原子性,有序性,可见性
		if(INSTANCE == null) {
            //synchronized保证没有原子性,有序性,可见性问题,但是synchronized临界区内仍然可能发生指令重排
			synchronized(Singleton.class) {
				//double check
				if(INSTANCE == null) {
					INSTANCE = new Singleton();
				}
			}
		}
		return INSTANCE;
	}
}

7. happens-before规则

遵循happens-before规则,能保证一个线程的写,对于另一个线程的读是可见的

  1. 线程解锁m前对变量的写,对于接下来加锁m的线程的读是可见的

  2. 对volatile变量的写,对其他线程对该变量的读是可见的

  3. 线程start前对变量的写,对线程开始后对该变量的读可见(线程刚启动会从主存读变量到工作内存

  4. 线程结束对变量的写,在其他线程得知它结束的读可见(线程结束前会将工作内存变量写入主存)

  5. 线程t1打断t2前对变量的写,对于其他线程得知t2被打断后对变量的读可见(也就是t1,打断t2,主线程去看t2的打断标记,也能看到之前t2的修改)

  6. 对默认值的写,对其他线程对该变量读可见

  7. 传递性: x hb-> y, y hb- > z ,x hb-> z

posted @ 2026-04-08 15:50  不会coding的喵酱  阅读(7)  评论(0)    收藏  举报