并行编程之volatile变量
在并行编程中,volatile变量算是一个最轻量级的同步规则了。volatile具有以下两种特性
保证变量的可见性
可见性是指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是,volatile的特殊性规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此,可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。
虽然volatile保证变量的可见性,但它并不保证原子性,我们需要自定义规则保证其原子性。
原子性:由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store、和write,我们大致可以认为基本数据类型的访问读写是具备原子性的(long,double例外)。
如果应用场景需要一个更大范围的原子性保证(经常会遇到),Java内存模式还是提供了Lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指导monitorenter和monitorexit来隐式地使用这两个操作,这两个字节码指令反应到java代码中就是同步块—-synchronized关键字,因此在synchronized块之间的操作也具备原子性。
关于保证其可见性但未保证原子性的可看如下代码
public class VolatileTest{ public static volatitle int race = 0; public static void increase(){ race++; } private static final int THREADS_COUNT = 20; public static void main(String[] args){ Thread[] threads = new Thread[THREADS_COUNT]; for(int i = 0 ; i < THREADS_COUNT; i++){ threads[i] = new Thread(){ @Override public void run(){ for(int i = 0 ; i < 1000;i++){ increase(); } } }; threads[i].start(); } while (Thread.activeCount() > 1){ Thread.yield(); } System.out.println(race); } }
这段代码的输出结果应该是20000,但是每次输出的结果都不一样,少于20000。问题就出现在reace++操作上。从字节码分析
public static void increate(); Code: Stack=2,Locals=0,Args_size=0 0: getstatic #13;//Field race:I 3: iconst_1 4: iadd 5: putstatic 8: return LineNumberTable: line 14:0 line 15:8
原因:但getstatic 指令把race 的值取到操作栈顶时,volatile保证了race的值此时是正确的,但是在执行iconst_1、iadd这些指令的时候,其他线程可能都已经把race的值加大了,而在操作栈顶的值变成了过期值所以putstatic指令可能把最小的race值同步到主内存中。
所以因为volatile变量只能保证其可见性,为了保证它的原子性我们还需要通过加锁(synchronized或java.util.concurrent)。
使用volatitle有以下两条原则- 运行结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
- 变量不需要与其他的状态变量共同参与不变约束
防止指令重排
public class Singleton{ private volatitle static Singletion instance; public static Singleton getInstance(){ if(instance == null){ synchronized(Singleton.class){ if(instance == null){ instance = new Singleton() } } } return instance; } public static void main(String[] args){ Singleton.getInstance(); } }
对上面代码可以使用加volatile和没有加volatile变量字节码对比。这段代码加上volatitle变量代码如下。
0x01a3de0f:mov $0x3375cdb0,%esi ;...beb0cd75 33 ; {oop('Singleton')} 0x01a3de14:mov %eax,0x150(%esi) ;...89865001 0000 0x01a3de1a:shr $0x9,%esi ;...clee09 0x01a3de1d:mov $0x0,0x1104800(%esi) ;...c6860048 100100 0x01a3de24:lock addl $0x0,(%esp) ;...f0830424 00 ;*putstatic instance ; - Singleton::getInstance@24
通过对比就会发现,关键变化在于有volatile修饰的变量,赋值后(前面mov%eax,0x150(%esi)这句便是赋值操作)多执行了一个”lock addl 0x0,( 0x0,(%esp)”(把ESP寄存器的值加0)显然是一个空操作(采用这个空操作而不是空操作指令nop是因为IA32手册规定lock前缀不允许配合nop指令使用),关键是在于lock前缀,查询IA32,它的作用是使得本cpu的cache写入内存,该写入动作也会引起别的cpu或者别的内核无效(Invaliadate)其cache,这种操作相当于对cache中的变量做了一次前面介绍Java内存模式中所说的”store 和write”操作。所以通过这样一个空操作,可让前面的volatile变量的修改对其他cpu立即可见。
摘自《深入理解Java虚拟机 JVM高级特效及最佳实践》