Java内存模型和volatile

硬件效率与一致性

  • CPU执行计算任务,需要从内存中读取和写入数据,但计算机存储设备和CPU的运算速度有几个数量级的差距。
  • 在内存和CPU之间增加一层或多层高速缓存作为CPU和内存之间的缓冲(高速缓存的读写速度接近处理器)。
  • 在多核处理器系统中,多个处理器任务都涉及同一块主内存区域时,可能会发生各缓存不一致的现象。
  • 引入缓存一致性协议:MSE、MESI、MOSI等。
  • 为了使处理器内部运算单元被充分利用,处理器可能会对输入代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果一致(计算的最终结果是一致的,但过程结果和顺序执行的过程结果未必一致)。

在这里插入图片描述

java内存模型

与上一节的交互关系很类似

  • 分为主内存和工作内存,主内存是虚拟机内存的一部分,每个线程有自己的工作内存。
  • 所有变量存储在主内存中(变量是指可以共享的变量,比如实例字段、静态字段、构成数组对象的元素等)。
  • 工作内存中保存变量副本,线程对变量的所有操作都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存。

在这里插入图片描述

内存间交互操作(8种)
一个变量如何从主内存拷贝到工作内存,如何从工作内存同步回主内存
lock(锁定):作用于主内存的变量,它把一个变量表示为一条线程独占的状态。
unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取): 作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中。
load(载入): 作用于工作内存的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个给变量赋值的字节码指令时,执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储): 作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中。
write(写入): 作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中。

  • 将变量从主内存拷贝到工作内存,需要顺序执行 readload(顺序,但不要求是连续)
  • 将变量从工作内存拷贝回主内存,需要顺序执行storewrite
  • 如果一个变量被assign了,就一定要同步回主存(assign->store->write
  • 如果对一个变量执行了lock,那么回清空工作内存中此变量的值,在执行引擎使用这个变量之前,需要重新执行load或assign操作以初始化该变量的值。
  • 对一个变量执行unlock之前,需要将该变量同步回主内存(store->write->unlock)

原子性、可见性和有序性

原子性

  • java内存模型直接保证的原子性变量操作包括:read、load、assign、use、store、write六种。可以认为,基本数据类型的访问、读写都具备原子性(除了long和double)。
  • 如果需要更大范围的原子保证,使用lock和unlock(未开放给用户),但提供了字节码指令monitorentermonitorexit来隐式使用lock和unlock。这两个字节码指令在java代码中就是synchronized关键字

可见性
当一个线程修改了共享变量的值,其他线程可以立刻得知这个修改。

  • volatile:新值立刻同步回主内存,每次使用前从主内存中刷新。
  • synchronized:unlock之前,要把变量同步回主内存(store->write->unlock)。
  • final:被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把"this"的引用传递出去,那么在其他线程中就能看见final字段的值。

有序性

  • volatile:禁止指令重排
  • synchronized:一个变量在同一个时刻只允许一条线程对其进行lock操作(持有同一个锁的代码块只能串行执行)。

volatile

  • 可以实现可见性:
    被volatile修饰的变量,修改后立刻同步回主内存。线程每次使用变量前会重新从主内存中刷新变量。(也就是平时工作内存中的变量副本可能存在缓存不一致的现象,但是每次使用前都刷新,所以执行引擎看不到不一致,因此可以认为不存在不一致的现象)。
    对于volatile变量的写操作会生成lock汇编指令(相当于内存屏障),导致将缓存回写到主存中,并且使其它线程的工作内存的此变量失效。
    read->load->use(load和use要连着,保证每次使用变量前都必须先从主内存中刷新最新的值,也就是保证能看见其他线程对变量进行的修改)
    assign->store->write(assign和store要连着,也就是每次修改变量后,都必须立刻同步回主存)
  • 禁止指令重排序优化:
    a,b都是被volatile修饰的变量
    如果assign a 比assign b先执行,那么read a先于read b,load a先于load b
    volatile修饰的变量不会被指令重排序优化,从而保证代码的执行顺序和程序的顺序相同。
如果第二个操作是volatile写,不管第一个操作是什么,都不能重排序,保证volatile写之前的操作不会被编译器重排序到volatile写之后
如果第一个操作是volatile读,不管第二个操作是什么,都不能重排序,保证volatile读操作之后的操作不会被编译器重排序到volatile读之前。

插入内存屏障
在volatile写操作前面插入StoreStore屏障,后面插入StoreLoad屏障

  • Store1;StoreStore;Store2
    在Store2及后续的写入操作执行之前,保证Store1的写入操作对其他处理器可见
  • Store1;StoreLoad;Load2
    在Store2及后续的读取操作执行之前,保证Store1的写入操作对所有处理器可见

在volatile读操作后面插入LoadLoad屏障,后面插入LoadStore屏障

  • Load1;LoadLoad; Load2
    在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • Load1; LoadStore; Store2
    在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

不保证原子性:
所以在并发的环境下是不安全的,需要通过加锁(synchronized、juc锁或原子类)来保证原子性。

一个使用例子:DCL单例模式

public class Singleton{
	private volatile static Singleton instance;
	private Singleton(){}
	public static Singleton getInstance(){
		if(instance == null){
			synchronized(Singleton.class){
				if(instance == null){
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
	public static void main(String[] args){
		Singleton.getInstance();
	}
}

参考资料
《深入理解Java虚拟机》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值