一个关于Java volatile关键字可见性和原子性的问题

一个变量arg用volatile修饰后,会直接保存在主存中
有两个线程A和B访问它

A和B同时将arg读取到了工作内存
若A对arg进行修改后,会导致B工作内存中的arg缓存无效
所以线程B需要再次从主存中读取arg
这就保证了线程B读取的是arg的最新值

问题:
线程A和线程B都对arg变量进行++操作

++操作的过程为,先读取arg,对arg加1,然后写回主存

假设:
arg初始值为0
线程A读取了arg,阻塞
线程B读取arg,对其++,并写回主存,此时arg=1
根据可见性,线程A工作内存中的arg变量应该会失效
此时线程A需要重新从主存中读取arg=1,然后进行++操作,将结果2写回内存

然而:
看了一些博客,都没有涉及到加粗文字的步骤。

clipboard.png

按照这样的说法,arg会被写两次,每次都是1。
线程A读取arg放入工作内存后,线程B的写操作不会影响线程A工作内存中arg变量的缓存。

问:这时候可见性不发挥作用么?

阅读 4.8k
5 个回答

volatile保证你每次读取都能读到最新的值,可是并不会更新你已经读了的值,它也无法更新你已经读了的值。

可见性的解释应该是保证多个线程对该变量(内存中的某个区域)的“读”是最新。

http://gee.cs.oswego.edu/dl/c...

Declaring a field as volatile differs only in that no locking is involved. In particular, composite read/write operations such as the "++'' operation on volatile variables are not performed atomically.

所以在这种组合读写的场景下,可见性只是不能保证最后的结果,但是它确实在发挥作用。

Using volatile fields can make sense when it is somehow known that only one thread can change a field, but many other threads are allowed to read it at any time.

这也是大神推荐的使用场景。

另外,我们可以反过来考虑,
假设初始x=0,t1和t2均读取到x=0,接下来t1设置x=1之后,
如果t2对x设置值=1,
问1.系统为什么会认为这个操作失效?
问2.失效后怎么处理?

  1. t2在设置x的时候,系统会自动把x=0带上去比较之前x的值?

    CAS,除非java把此类的变量都编译成带CAS操作的字节码。
    
  2. 假设也存在以上这一的机制,t2执行x=1失效之后会有什么操作?

    抛异常?程序捕获然后又重新读取设置?又回到CAS上面了。
    

希望对题主有所帮助,如有遗漏欢迎指正。

没有什么工作内存、主内存,对编译器来说内存和Cache的区别是不可见的,编译器关心的是内存和寄存器,如果你非要把寄存器叫做工作内存也不是不可以但是很别扭……volatile影响的是编译器在连续的多个语句之间是否可以假设这个变量没有被别人修改,这样就可以继续用之前在寄存器里面的值。否则编译器总是去内存里读一个新的值出来,而不使用寄存器里面暂存的值。它也不关心这其中有没有别人实际修改了这个值。至于多核系统内存和Cache之间的关系,在x86架构上这是CPU自己会处理的问题,在一些别的架构上可能会设计一些特殊的指令。
也就是说根本就没有你说的一个修改导致另一个失效那样子的魔法过程。如果另一个线程是在++的执行过程中,刚读出来的值被修改了,它当然是没有办法用超能力去预知这个事情然后重新读一次的。

  1. 首先,使用volatile,工作内存的概念依然存在,只是会通过读写屏障来达到直接读写内存的效果。所以,你可以认为:volatile变量直接操作内存(虽然严格说并不是)

  2. 读值是指读到cpu,后cpu对值++,然后重新写回内存(或工作内存),读到的值指的是读到CPU的值,这个是没法因为内存值的改变而自动改变的。

新手上路,请多包涵

我也被这个问题困扰了很久,实际结果通过实验我们都知道,确实是不能保证原子性,那应该怎么去理解为什么会这样呢?我个人是这样理解的,线程A和线程B一开始都读取到的是初始值0,线程A进行++操作,线程A中的工作内存中arg的值变成1,此时发生阻塞,还没有写回主内存,线程B也去进行arg++操作,线程B的工作内存中arg的值也变成1,然后线程B进行lock,写回主内存,主内存的arg=1,同时线程A因为总线嗅探机制,会把线程A中的工作内置中的arg值变成失效,然后会重新load主线程中arg=1的值,此时线程A中的arg就是从主线程load过来的值了,虽然加载过来了,但是因为线程A阻塞的地方是在arg++运算结束之后,因此不会再去进行arg++操作,而是进行后面的lock,再把arg=1更新回主内存。因此两个线程都运行完,主内存中的值还是1。那如果线程A是在arg+1运算之前就阻塞了,那么失效后再重新加载进来,再通过arg++运算,那最终的值应该就是正常的2. 所以我认为出现值不准确主要还是因为阻塞的时候,是在线程A刚好运算完,写回主内存之间发生导致的

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏