java中volatile的作用,volatile保证可见性有序性原理,volatile不保证原子性

java中volatile的作用
一、保证可见性

​ 作用:一个共享变量被volatile修饰时,它会保证将修改后的值立刻更新会主存,当有其他线程需要读取它时,它会去内存读取新值

​ 普通的共享变量不保证可见性,普通变量被修改后什么时候写回主存是不确定的,当其他线程去读取时,内存中可能还是原来的旧值,因此无法保证可见性

​ synchronized和Lock也能保证可见性,synchronized和Lock能够保证同一时间内只有一个线程获取锁然后执行同步代码,然后在释放锁后将变量的修改刷新到主存中

​ 例子:

boolean stop = false;
//线程一循环
while(!stop){
doSomething();
}
//线程2中断线程1循环
stop = true;

这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。

​ 原因:每个线程在运行过程中都有自己的工作内存区,线程一在工作时会将stop变量的值拷贝一份放在自己的工作内存中,当线程2更改进程变量时,还没来得及写入主存,转去做其他事了,线程1不知道线程2对stop变量的修改,会一直陷入死循环。

用volatile修饰变量后

  • 用volatile关键字会强制将修改的值立即写入内存
  • 使用volatile关键字的话,线程2进行修改时,会导致线程1再次读取stop变量的缓存行无效
  • 由于线程一的工作内存中的变量stop缓存行无效,所以线程再一次读取stop的值时会去主存读取

那么在线程2修改stop值时(当然这里包括2个操作, 修 改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。那么线程1读取到的就是最新的正确的值。

科普

​ 当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致,比如说:变量在多个CPU之间的共享。

​ 如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢?

​ 为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI、MOSI、Synapse、Firefly及DragonProtocol等。

​ MESI(缓存一致性协议)

​ 当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

​ 那么问题来了,为什么volatile变量在被一个线程写操作时会使其他线程里的工作缓存无效,怎么发现数据是否失效呢?

  • 通过嗅探

    每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

  • 缺点:由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和cas不断循环,无效交互会导致总线带宽达到峰值。

    • 所以不要大量使用Volatile,至于什么时候去使用Volatile什么时候使用锁,根据场景区分。
二、禁止重排序,保证有序性

​ 指令重排序:一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它保证程序最终执行结果和代码顺序执行的结果是一致的。

​ 处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

//下面两个语句就有依赖性
a = a + 3; 
r = a*a;   

详细讲解Java并发编程:volatile关键字解析 - Matrix海子 - 博客园 (cnblogs.com)

volatile boolean inited = false;
//线程1:
//loadContext(),加载配置
context = loadContext();  //语句1
//加载完配置才改变inited的值,inited配置是否完成的标志
inited = true; 			  //语句2

//线程2:当配置完成后进行操作
while(!inited ){
sleep()
}
//初始化结束后,进行某些操作
doSomethingwithconfig(context);

​ 因为语句1和语句2没有依赖性,所以可能被重排序,假如发生重排序,语句2先执行,inited = true,线程2以为初始化工作已经结束,挑出while循环。执 行doSomethingwithconfig方法,但此时context还没被初始化,会导致程序出错

​ 指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

​ 在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

​ 如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕

《深入理解Java虚拟机》:“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

(2)它会强制将对缓存的修改操作立即写入主存;

(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

三、使用volatile关键字的场景

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

(1)对变量的写操作不依赖于当前值//比如说a+=1

(2)该变量没有包含在具有其他变量的不变式中//a=i

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

四、volatile不保证原子性

​ Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再将i的值赋值给j,两个原子操作加起来就不是原子操作了。

​ 如果一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行自增这样的非原子操作,就不会保证这个变量的原子性了。

​ 例子:

	一个变量i被volatile修饰,两个线程想对这个变量修改,都对其进行自增操作也就是i++,i++的过程可以分为三步,首先获取i的值,其次对i的值进行加1,最后将得到的新值写会到缓存中。

​ 线程A首先得到了i的初始值100,但是还没来得及修改,就阻塞了,这时线程B开始了,它也得到了i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中。根据可见性的原则,这个主存的值可以被其他线程可见。
​ 问题来了,线程A已经读取到了i的值为100,也就是说读取的这个原子操作已经结束了,所以这个可见性来的有点晚,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存,本来设想两次自增过后i的值为102所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。

参考链接:

(51条消息) volatile为什么不能保证原子性_Blog-CSDN博客_volatile为什么不能保证原子性

Java并发编程:volatile关键字解析 - Matrix海子 - 博客园 (cnblogs.com)

纯手打不易,望点赞支持

  • 9
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值