volitale关键字及底层原理

Volitale关键字

​ volatile是一个特征修饰符(type specifier),它是Java虚拟机提供最轻量级的同步机制,volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。

Volitale的作用

作用一 可见性

  • volitale保证了被它修饰的变量对所有线程的可见性,这里的‘‘可见性’‘是指当一条线程修改了这个变量的值,其他线程是可以立刻得知修改后的新值。接下来从一个案例和JMM角度来分析volitale如何体现可见性的。

    • 先写一个不加volitale关键字的例子

      //定义个变量
      int a = 1;
      //Thread1内执行的代码
      a = 2;
      //Thread2内执行的代码
      b = a;
      

      Thread1和Thread2初始化时,会将两个变量读取到本地内存(或叫工作内存)中,当Thread1执行a=2后,并没有立即写到主存中,Thread2在执行b=a的时候读到的值还是1,而不是Thread改变之后的2。如下图
      在这里插入图片描述

    • 先写一个不加volitale关键字的例子

      //定义个变量
      volitate int a = 1;
      //Thread1内执行的代码
      a = 2;
      //Thread2内执行的代码
      b = a;
      

      Thread1和Thread2初始化时,还是会将两个变量读取到本地内存中,但是当Thread1执行a=2后,会强制将a的新值2写入到主存中,并且使其他线程里的缓存失效,Thread2就会重新从主存中取a的新值2,Thread2在执行b=a时,得到的值就是2了。
      在这里插入图片描述

注意:

  • JMM是个抽象的内存模型,所以所谓的本地内存,主内存都是抽象概念,并不一定就真实的对应cpu缓存和物理内存
  • volatile并不能保证原子性(volatile修饰的变量进行复合操作时(a++)就会出现并发安全问题)
  • volitale保证原子性的三种方法:
    • 加synchronized关键字
    • 加lock锁
    • 使用原子类(Atomic)借助CAS来实现

作用二 禁止指令重排序优化

​ volatile 变量的禁止指令重排是基于内存屏障(Memory Barrier)实现。接下来依次介绍一下什么是指令重排,内存屏障以及volitale关键字底层是如何使用内存屏障来实现禁止指令重排的。

  • 指令重排

    • 重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。

    • 重排序需要遵守的规则:

      • 重排序操作不会对存在数据依赖关系的操作进行重排序。比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
      • 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。
  • 内存屏障

    • 定义:内存屏障,也称内存栅栏,内存栅障,屏障指令等, 是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。

    • 了解两个指令:

      • Store:将处理器缓存的数据刷新到内存中。
      • Load:将内存存储的数据拷贝到处理器的缓存中。
    • 内存屏障的四种类型:
      在这里插入图片描述

    • java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表。
      在这里插入图片描述
      对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM采取了保守策略:

      1. 在每个volatile写操作的前面插入一个StoreStore屏障;
      2. 在每个volatile写操作的后面插入一个StoreLoad屏障;
      3. 在每个volatile读操作的后面插入一个LoadLoad屏障;
      4. 在每个volatile读操作的后面插入一个LoadStore屏障。

      需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障

      StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;

      StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序

      LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序

      LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序

    • volatile的内存屏障插入示意图

      • volatile写插入内存屏障示意图
        在这里插入图片描述

      • volatile读插入内存屏障示意图
        在这里插入图片描述

      总结:上面的原理理解起来就是,volatile确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面,即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。

      代码层面来说就是,对volitale变量执行的代码都是按照程序执行的顺序来的,不会因为底层指令重排而颠倒代码执行顺序,由此来实现有序性(个人理解)

Volitale的底层指令实现

0x01a3de0f:		mov			0x3375cdb0,%esi;		;...beb0cd75 33
													;	{oop('Singleton')}
0x01a3de14:		mov 		%eax,0x150(%esi)		;...89865001 0000
0x01a3de1a:		shr			0x9,%esi				;...c1ee09
0x01a3de1d:		movb		0x0,0x1104800(%esi) 	;...c6860048 100100
0x01a3de24:		lock 		addl0x0,(%esp)			;...f0830424 00
                                                	;*putstatic instance
                                                	;-
Singleton::getInstance@24

通过观察汇编代码发现,有volatile修饰的变量,赋值后(mov %eax,0x150(%esi)这句就是赋值操作)多执行了一个“lock addl0x0,(%esp)”操作,这个操作就相当于一个内存屏障,只有一个CPU访问内存时,不需要内存屏障,但如果有两个或更多CPU访问一块内存时,且启用一个在观测另一个,就需要内存屏障来保证了有序性。lock前缀的作用还使得本CPU的Cache写入内存,该动作也会引起别的CPU或者别的内核缓存失效,也就实现了可见性。

参考链接:

https://www.jianshu.com/p/157279e6efdb

https://baijiahao.baidu.com/s?id=1667840029586081215&wfr=spider&for=pc

《深入理解JVM虚拟机》

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值