volatile关键字解析
volatile 保证此变量对所有线程的可见性。
这个可见性是指,当一个线程读取volatile修饰的变量时,永远读取的都是最后一个线程写回主内存的最新值。某个线程在读取数据之后,另一个线程对变量值做了修改,这个线程是不知道的,这就导致当前线程读取的值是过期的,当前线程将过期的数据经过计算写回主内存时,就会出现问题。看下面代码:
public class VolatileTest {
public static volatile int race = 0;
/**
* 每次都对race累加
*/
public static void increase() {
race++;
}
private static int THREAD_COUNT = 20;
public static void main(String[] args) {
// start 20 thread, every thread invoke increase function 20 times
for (int tCount = 0; tCount < THREAD_COUNT; tCount++) {
new Thread(() -> {
for (int i = 0; i < 10000; i++) {
increase();
}
System.out.println(Thread.currentThread().getName() + "is finished");
}).start();
}
/**
* 当还存在线程运行时,不结束主线程。
*/
while (Thread.activeCount() > 1) {
System.out.println("main thread yield, the active count is " + Thread.activeCount());
Thread.yield();
}
// in theory, the result is 20 * 10000 = 200000
System.out.println(race);
}
}
看结果,race变量的值基本上不会是20000。
javap -verbose 类文件, 查看方法的字节码片段
给出方法编译的字节码
public static void increase();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field race:I
3: iconst_1
4: iadd
5: putstatic #2 // Field race:I
8: return
LineNumberTable:
line 14: 0
line 15: 8
字节码还是变成了几个命令行,假如当前线程执行了getstatic时volatile修饰的变量是正确的,当在执行iconst_1和iadd时,其他线程可能将主内存中的race值加大了,当前线程将变量写回主内存时,就会覆盖之前的值,导致race的值一直累加不到20000。
通过以上分析,发现如果需要保证方法的原子性还是需要使用synchronized或者java.util.concurrent中的原子类。
volatile 的第二个作用是禁止指令重排优化。
先看一段代码:
public class Singleton {
private Singleton() {}
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
上述代码是基于双锁检测实现的单例模式。对比instance添加volatile修饰符前后汇编代码:
添加volatile之前,
添加volatile之后,
发现添加volatile修饰符后,汇编多生成了一条
0x0000000002b83350: lock add dword ptr [rsp],0h
add dword ptr [rsp],0h,意思是将双字节的栈指针寄存器加0,这句代码没有问题,关键是其前面的修饰符lock。lock的作用使得本CPU的Cache写入内存,该写入动作也会引起其他CPU或者核无效化,这样就保证了本次值的修改对其他CPU可见了。
lock是如何禁止指令重排的呢?指令重排是指CPU允许多条执行不按程序规定顺序执行。但如果指令之间如果有依赖,那么指令就不能重排。例如一个指令,给地址A加1,另一个指令为将地址A的数据乘以2,还有另一条指令是将B地址的数据加5。可以看出指令1和指令2之间是有依赖关系的,不能重排,但是对于指令3 将B地址的数据加5,和指令1和指令2没有依赖关系的,所以可以重排,而且重排完成后依然是有序的。当使用lock指令,将计算的数据更新到主内存后,意味着所有之前的操作都已经执行完成,这样便形成了“指令重排无法越过的内存屏障”。