JAVA内存模型之关键字volatile

JAVA内存模型之关键字volatile

volatile关键字可以说是Java虚拟机提供的最轻量级的同步机制,Java内存模型对volatile专门定义了一些特殊的访问规则。当一个变量定义为volatile后,它将具备两种特性:可见性和禁止指令重排序。

可见性

可见性是指一条线程改变了这个变量的值,新值对其他线程可立即得知,根据Java虚拟机规范的规定,volatile变量依然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般。volatile变量在各个线程中不存在一致性问题(在各个线程的工作内存中,volatile变量也可以存在不一致的情况,但是由于每次使用之前都要先刷新,所以执行引擎看不到不一致的情况,因此可以认为不存在一致性问题)。
Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的。如下代码所示

/**
volatile变量自增运算测试
**/
public class VolatileTest{
public static volatile int rece=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;i1){
Thread.yield();
}
System.out.println(race);
}
}
}

这段代码如果能正确的并发的话,最后输出的结果应该是200000,但是运行多次后,并没有出现预期的结果。问题就出在自增运算race++上,race++可以翻译为race=race+1;当取出race的时候,保证了当前值为最新值,但是在做race+1这个动作时,其他的值可能已经把主内存中race的值改变了,而此时当前线程race的值就是过期数据,所以同步race+1的值就是比预期的值要小。
由于volatile变量只能保证可见性,如果不符合下列两条规则的运算条件,还是需要通过加锁(synchronized或java.util.concurrent中的原子类)来保证原子性。

  • 运算结果并不依赖当前值,或者能够确保只有单一的线程修改变量的值。
  • 变量不需要与其他的状态变量共同参与不变约束。

禁止指令重排序

从硬件架构上来讲,指令重排序是指cup采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理,但并不是说指令任意重排,cpu需要能正确处理指令依赖情况以保障程序能得出正确的执行结果。
普通变量仅仅会保证在该方法执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值 操作的顺序与程序代码中的执行顺序一样。我们可以通过一段代码看一下指令重排序如何干扰程序的并发执行。

Map configOptions;
char[] configText;
volatile boolean initialized=false;
configOptions=new HashMap();
configText=readConfigFile(fileName);
processConfigOptions(configText,configOptions);
initialized=true;
while(!initialized){
sleep();
}
doSomethingWithConfig();

上述伪代码描述的场景十分常见,只是我们在处理配置文件时一般不会出现并发而已,如果定义initialized变量时没有用volatile变量修饰,就可能会由于指令重排序的优化导致位于县城A中的最后一句代码"initialized=true"被提前执行,这样在线程B中使用配置信息的代码就可能出现错误,而volatile关键字则可以避免此类情况发生。
volatile屏蔽指令重排序的语义在Jdk1.5中才被完全修复,此前的jdk及时将变量声明为volatile也仍然不能完全的避免重排序所导致的问题(主要是volatile变量前后的代码任然存在重排序的问题),这点也是在jdk1.5之前的java中无法安全使用DCL(双锁检测)来实现单例模式的原因。

如下代码是一段标准的DCL单例代码

public class Singleton{
private volatile static Singleton 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修饰的变量,赋值后多执行了一个 "lock addl $0X0,(%esp)"操作(这个操作是包esp寄存器的值加0,是一个空操作,IA32手册规定lock前缀不允许配合nop空操作指令使用),这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存重排序之前的位置),只有一个cpu访问内存时,并不需要内存屏障,但如果有多个cpu同时访问同一块内存,且其中一个在观测另一个,就需要内存屏障来保持一致性。指令的lock前缀的作用是使得本cpu的cache写入了内存,该写入动作也会引起别的cup或者别的内核无效化其cache。这种操作相当于对cache中的变量做了一次java内存模型的store和write操作,所以可让前面volatile变量的修改对其他cpu立即可见。
volatile的同步机制的性能在某些情况下确实要优于锁,但是由于虚拟机对锁实行的许多消除和优化,使得我们很难量化的认为volatile就比synchronized快多少。如果volatile和自己比较,可以确定一个原则,volatile变量的读操作的性能和普通变量几乎没有差别,但是写操作可能会慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。不过即便如此大多数场景下,volatile的总开销任然要比锁低,我们在volatile与锁之中选择的唯一依据仅仅是volatile的语义能否满足使用场景的需求。

Java内存模型对volatile变量定义的特殊规则

假定T表示一个线程,V和W分别表示两个volatile型变量,那么在进行read、load、use、assign、store和write操作时需要满足如下规则:

  • 只有当线程T对变量V执行的前一个动作是load的时候,线程T才能对变量V执行use动作。并且,只有当线程T对变量V执行的后一个动作是use的时候,线程T才能对变量V执行load动作。线程T对变量V的use动作可以认为是和线程T对变量V的load、read动作相互关联,必须连续一起出现(这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改后的值)
  • 只有当线程T对变量V执行的前一个动作是assign的时候,线程T才能对变量V执行store动作;并且,只有当线程T对变量V执行的后一个动作是store时,线程才能对变量V执行assign动作。线程T对变量V的assign动作可以认为是和线程T对变量V的store、write动作相互关联,必须连续一起出现(这条规则要求在工作内存中,每次修改V后都必须立刻同步会主存中,用于保证其他线程可以看到自己对变量V所做的修改)
  • 假定动作A是线程T对变量V实施的use或assign动作,假定动作F是和动作A相关联的load或store动作,假定动作P是和动作F相应的对变量V的read和write动作。假定动作B是线程T对W实施的use或assign动作,假定动作G是和动作B相关联的load和store动作,假定动作Q是和动作G相对应的read或write动作。如果A先于B,那么P先于Q(这条规则要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同)。

你可能感兴趣的:(java)