Java中long和double赋值不是原子操作,因为先写32位,再写后32位,分两步操作,这样就线程不安全了。如果改成下面的就线程安全了
private volatile long number = 8;
那么,为什么是这样?volatile关键字难道可以保证原子性?
java程序员很熟悉的一句话:volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性。但是我们这里的例子,volatile似乎是有时候可以代替简单的锁,似乎加了volatile关键字就省掉了锁。这不是互相矛盾吗?
其实如果一个变量加了volatile关键字,就会告诉编译器和JVM的内存模型:这个变量是对所有线程共享的、可见的,每次jvm都会读取最新写入的值并使其最新值在所有CPU可见。所以说的是线程可见性,没有提原子性。
下面我们用一个例子说明volatile没有原子性,不要将volatile用在getAndOperate场合(这种场合不原子,需要再加锁,如i++),仅仅set或者get的场景是适合volatile的。
例如你让一个volatile的integer自增(i++),其实要分成3步:
- 1)读取volatile变量值到local;
- 2)增加变量的值;
- 3)把local的值写回,让其它的线程可见。
这3步的jvm指令为:
mov 0xc(%r10),%r8d ; Load inc %r8d ; Increment mov %r8d,0xc(%r10) ; Store lock addl $0x0,(%rsp) ; StoreLoad Barrier
注意最后一步是内存屏障。
什么是内存屏障(Memory Barrier)?
内存屏障(memory barrier)是一个CPU指令。基本上,它是这样一条指令:
- a) 确保一些特定操作执行的顺序;
- b) 影响一些数据的可见性(可能是某些指令执行后的结果)。
编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。
插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。
内存屏障另一个作用是强制更新一次不同CPU的缓存。
例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。
内存屏障(memory barrier)和volatile什么关系?上面的虚拟机指令里面有提到,如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。这意味着如果你对一个volatile字段进行写操作,你必须知道:1、一旦你完成写入,任何访问这个字段的线程将会得到最新的值。
2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
分析
结合内存屏障这个概念对volatile的读写操作深入理解的话:
第一步:读
在第一步操作的指令后,会增加两个内存屏障:
- 在Volatile读操作后插入LoadLoad屏障,防止前面的Volatile读与后面的普通读重排序
- 在Volatile读操作后插入LoadStore屏障,防止前面的Volatile读与后面的普通写重排序
因此第一个指令和它后续的普通读写操作会被保证没有重排序来捣乱。通常是去内存中去读。
那么问题又来了,为什么通常去内存中读?
其实这个问题要说细的话可以很细,大概就两个关键点吧:
- volatile的写操作的缓存失效机制
- 最后一个对volatile变量执行写操作的CPU,由于在它对应的缓存中保有最新的值,因此可以不用再去主存里面获取
具体看下面第三步的分析。
第二步:自增
这个步骤没什么特别的,就是在CPU自身的高速缓存(寄存器,L1-L3 Cache)中完成。不涉及到缓存和内存的交互。
第三步:写
volatile写算是一个重点。
根据JMM对于volatile变量类型的语义规范:volatile在编译之后,会在变量写操作时添加LOCK前缀指令。这个LOCK前缀指令在多核处理器的环境中,有这样的作用:
- 通知CPU将当前处理器缓存行的数据写回到系统主存中
- 该写回操作将使其他CPU缓存了该内存地址的数据无效
另外,内存屏障在volatile的写操作中起到了很大的作用,来保证上面两点能够实现:
- 在Volatile写操作前插入StoreStore屏障,防止前面其他写与本次Volatile写重排序
- 在Volatile写操作后插入StoreLoad屏障,防止本次的Volatile写与后面的读操作重排序