JDK官方文档是这样形容volatile的:
The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes. A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable.
意思就是说,如果一个变量加了volatile关键字,就会告诉编译器和JVM的内存模型:这个变量是对所有线程共享的、可见的,每次jvm都会读取最新写入的值并使其最新值在所有CPU可见。volatile似乎是有时候可以代替简单的锁,似乎加了volatile关键字就省掉了锁。但又说volatile不能保证原子性(java程序员很熟悉这句话:volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性)。
我们都知道Java volatile关键字是为了保证共享变量在多线程间的可见性,即某个线程修改了共享变量的值,其他线程可以能够立即读到这个修改后的值。可见性的实现原理可以简单的概况如下:volatile变量进行写操作时,JIT编译器会在生成的汇编指令后加上一个lock前缀的额外指令,这个lock指令会使得处理器缓存立即回写到主内存,并使得其他处理器缓存的该缓存行无效。
前面讨论了volatile变量的可见性,那么现在说说它的原子性,先看看下面的代码:
package com.company;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Created by root on 2017/10/14.
*/
public class VolatileTest {
private static volatile int anIntV = 0;
private static AtomicInteger anIntA = new AtomicInteger(0);
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(4);
new Thread(() -> {
for (int i = 0; i < 200000; i++) {
anIntV++;
anIntA.getAndIncrement();
}
latch.countDown();
}).start();
new Thread(() -> {
for (int i = 0; i < 200000; i++) {
anIntV++;
anIntA.getAndIncrement();
}
latch.countDown();
}).start();
new Thread(() -> {
for (int i = 0; i < 200000; i++) {
anIntV++;
anIntA.getAndIncrement();
}
latch.countDown();
}).start();
new Thread(() -> {
for (int i = 0; i < 200000; i++) {
anIntV++;
anIntA.getAndIncrement();
}
latch.countDown();
}).start();
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("anIntV = " + anIntV);
System.out.println("anIntA = " + anIntA);
}
}
这段代码的执行结果如下
不同于anIntA每次执行结果都是800000,而且这里的anIntV每次执行的结果都不一样。我们可以先下结论,volatile变量没有保证变量的原子性。
我们回头来看看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
注意最后一步是内存屏障。
如果变量是volatile 变量,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。这意味着如果你对一个volatile字段进行写操作,你必须知道:1、一旦你完成写入,任何访问这个字段的线程将会得到最新的值。2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
再回来看前面的JVM指令:从Load到store到内存屏障,一共4步,其中最后一步jvm让这个最新的变量的值在所有线程可见,也就是最后一步让所有的CPU内核都获得了最新的值,但中间的几步(从Load到Store)是不安全的,中间如果其他的CPU修改了值将会丢失。