返回信息流在学习volatile过程中,看了很多关于volatile为什么不保证原子性的博客,看完后存在一些疑点。
假设有两个线程对一个volatile变量i进行自增操作,即i++,执行i++指令分为三步进行:读取i值、i+1、将运算结果赋值给i并写主存。
大部分博客的解释是:假如某个时刻变量i的值为10,线程1首先执行,在读取了变量i的值后线程1就被阻塞了;然后线程2开始执行,读取变量i的值为10,然后进行加1操作,并把运算结果11写回了主存,线程2执行结束。紧接着线程1又开始执行,由于已经完成了读取了i的这一步操作,所以紧接着执行i+1操作,最终的运算结果也为11。
我的疑问是:根据可见性原则,当一个线程对volatile变量进行写操作时,其余线程中该变量的缓存行无效,所以线程2修改了变量i的值,线程1中变量i的缓存将无效,线程1第二次运行时因为第一次读取的i值无效难道不应该再读一次i值了吗?
我自己的理解:线程1首先读取了i的值10,然后执行了+1操作,但还没有将计算结果赋值给i就阻塞了(此时count的值还未改变),线程2开始执行,首先读取i值为10,执行+1操作,并将计算结果11赋值给i并写回主存。线程1又开始运行,根据可见性的原则,线程1能看到count的最新值1,但是因为其已经进行了第二步操作,所以接一下执行第三步操作,将计算结果赋值给i写回内存,计算结果也为11.
希望有大佬解释一下哪个对哪个错,还是这两种解释都有问题。谢谢!
这是一条镜像帖。来源:北邮人论坛 / java / #64327同步于 2020/8/23
该镜像源已超过 30 天没有更新,可能在源站已被删除。
Java机器人发帖
请教volatile为什么不保证原子性
zsw1102
2020/8/23镜像同步11 回复
订阅后,新回复会通过你的通知中心匿名送达。
9 条回复
根据.NET的答一下吧,估计也差不多。缓存是指CPU缓存,CPU缓存一致性协议会告诉其他CPU缓存失效。但是你计算i++的时候是将memory load到寄存器中进行计算,线程1和线程2都将i load到寄存器中(10),执行加1后还是存在寄存器中(11),这时候和缓存失效没关系,然后写会主存(11),建议你可以看下最终的机器码就明白了。
volatile的write操作原子性c#应该是保证的,因为比如long它就不能定义为volatile,因为它的load和store操作不能有一个CPU指令来完成,
但是针对i++这个操作,本身他就是load add store,多个命令才能完成的,store是atomic的,但是你之前的命令并不是。在.NET中可以使用Interlocked.Increment做自增操作,使用了lock inc指令保证原子性
(仅供参考)
volatile应该是解决 读了i,然后把它放缓存里,下次又要读它的时候就不从内存读了,直接把缓存拿来用 的情况。
这属于编译器优化,volatile防止这种编译器优化由于对其他线程缺少知识而造成的非预期行为,但是volatile不能让你本来就该读的而且已经读了的再去读一遍。
如果这里不是i自加,而是a = i + 1,那么这里a在阻塞之前就完成赋值和在阻塞之后才完成赋值的预期应该是程序去考虑,而不是编译器。
编译器只能保证程序执行的序是正确的,没法保证也没法知道几个程序之间应该是什么样的序(不然多线程就都不用加锁了)。
对一个volatile变量进行自增操作时,要先load再自增然后store,jvm会在store指令后面加上一个内存屏障,这个内存屏障的作用是禁止屏障之前的指令被重排序到屏障后面,同时store指令会被加上lock指令,这两个保证了读写的顺序性以及内存可见性,但是不能保证原子性,因为load自增store三个指令不是原子的,其他线程的指令可以插入到这三个指令之间,所以其他线程的store的结果可以被当前线程覆盖,因此不能保证原子性
volatile变量在读写时受内存屏障保护,简单举个例子: volatile int a 等价于 有个类
...
public class A
{
int value;
synchronized int getValue(){
return value;
}
synchronized void setValue(int value){
this.value = value;
}
}
...
此时 volatile int a => A a = new A(); a++ => int oldA = a.getValue(), a.setValue(oldeValue + 1)。可以看出getValue 和setValue虽然是原子性的,但不代表int oldA = a.getValue(), a.setValue(oldeValue + 1)也具有原子性。
老生常谈了。看这里:
https://bbs.byr.cn/article/Java/39220
简单地说:
- volatile保证读是原子的吗?是。
- volatile保证写是原子的吗?是。
- 对volatile变量用x++或者x+=y的形式做“读、改、写”操作,整体原子吗?不
建议:用java.util.concurrent.atomic.AtomicInteger
另外,这里完全不涉及缓存。甚至连happens-before关系都暂且不涉及。就是个word tearing和多线程并发操作的问题
CPU寄存器 缓存 主存 辅助存储器这个层次如果弄明白,那么很容易知道 volatile 下,缓存失效只会在读操作时发觉,但是如果已经处于计算、写步骤时,并不能够觉察到缓存失效,因此会导致非原子性。
暖神暖神,问一个问题。今天同事说到atomicxxx类,说建议高并发的时候不要用,因为虽然jdk中实现是一个自旋锁,但是会锁cpu。这种说法正确吗?
我感觉他也不是很清楚
【 在 nuanyangyang 的大作中提到: 】
: 老生常谈了。看这里:
: https://bbs.byr.cn/article/Java/39220
: 简单地说:
: - volatile保证读是原子的吗?是。
: - volatile保证写是原子的吗?是。
: - 对volatile变量用x++或者x+=y的形式做“读、改、写”操作,整体原子吗?不
: 建议:用java.util.concurrent.atomic.AtomicInteger
: ............
毫无道理。首先不太可能实现为自旋锁,另一方面没有所谓的“锁cpu”的说法
它一般依赖机器的特殊指令实现。有的架构(如x86、armv8.2)有lock add这样的指令,另一些(如armv6、7、8、riscv、power)使用load-link store-conditionally。自己查查吧。
欧克克
【 在 nuanyangyang 的大作中提到: 】
: 毫无道理。首先不太可能实现为自旋锁,另一方面没有所谓的“锁cpu”的说法
: 它一般依赖机器的特殊指令实现。有的架构(如x86、armv8.2)有lock add这样的指令,另一些(如armv6、7、8、riscv、power)使用load-link store-conditionally。自己查查吧。
: --
: ............