volatile关键字总结

1、什么是volatile

volatile是java的一个关键字,它提供了一种轻量级的同步机制。相比于重量级锁synchronized,volatile更为轻量,因为它不会引起线程上下文的切换和调度。

2、volatile的两个作用

可以禁止指令的重排序优化。

提供多线程访问共享变量的内存可见性。

3.1什么是指令重排

指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度,例如将多条指令并行执行或者是调整指令的执行顺序。但是在多线程的情况下,指令重排序可能会带来问题,例如程序执行的顺序可能会被调整。在加上volatile关键字后可以有效解决这个问题。

下面我们举个例子:

double r =2.1;//1

double pi =3.14;//2

double area=pi*r*r;//3

以上代码语句的执行顺序为1-》2-》3,但实际上顺序无论是1-2-3还是2-1-3对结果并无影响,所以在编译时和运行时可以根据需要对1、2语句进行重排序。

重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段,重排序需要遵守一定规则:

1 不会对存在数据依赖关系的操作进行重排序

2 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变

3.2 指令重排带来的问题

基于双重检验的单例模式:

public class Singleton3(

    private static Singleton3 instance = null;

    private Singleton3(){}

    public static Singleton3 getInstance(){

        if(instance==null){

            synchronized(Singleton3.class){

                if(instance==null)

                    instance=new Singleton3();//非原子操作

            }

        }

        return instance;

    }

)

事实上,这个单例模式的实现方式是有问题的,问题在于instance=new Singleton3();并不是一个原子操作。

我们可以将其抽象成以下几条指令:

memory=allocate();//1:分配对象的内存空间

ctorInstance(memory); //2:初始化对象

instance = memory; //3:设置instance指向刚分配的内存地址

可以看到,操作2依赖于操作1,但操作3并不依赖于操作2,所以JVM是可以针对它们进行指令的优化重排序的,经过重排序后如下:

memory=allocate();//1:分配对象的内存空间

instance = memory; //3:设置instance指向刚分配的内存地址

ctorInstance(memory); //2:初始化对象

指令重排之后,instance执行分配好的内存放在了前面,而这段内存的初始化被放在了后面,在线程A执行这段赋值语句,在初始化对象之前就已经将其赋值给instance引用,恰好另一个线程进入方法判断instance引用不为null,然后就将其返回使用,导致出错。

3.3 禁止指令重排的原理

volatile关键字提供内存屏障的方式来防止指令被重排,编译器在生成字节码文件时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

内存屏障会确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。

对于上面的基于双重检验的单例模式,我们只需对其稍作修改即可令其正确运行,我们已经知道,问题来自于指令重排,那么我们禁止掉指令重排即可,用volatile关键字修饰instance变量,使得instance在读、写操作前后都会插入内存屏障,避免重排序,完整代码如下:

public class Singleton3(

    private static volatile Singleton3 instance = null;

    private Singleton3(){}

    public static Singleton3 getInstance(){

        if(instance==null){

            synchronized(Singleton3.class){

                if(instance==null)

                    instance=new Singleton3();

            }

        }

return instance;

    }

)

4 保证内存可见性

4.1 什么是保证内存可见性

java支持多个线程同时访问一个对象或者是对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的是加速程序的执行,这是现代多核处理器的一个显著特性),所以程序在执行过程中,一个线程看到的变量不一定是最新的。volatile告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。

4.2 实现的具体细节

如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。

但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理区缓存里。

具体地说,内存可见性也是通过内存屏障实现的,它会执行下面两个操作:

强制将对缓存的修改操作立即写入主存;

如果是写操作,它会导致其他CPU中对应的缓存行无效

5 总结

volatile提供了一种轻量级的同步机制,在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞。

volatile只能确保可见性,而加锁机制既可以确保可见性又可以确保原子性。

volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低。

相比于synchronized,虽然volatile更简单并且开销更低,但是它的同步性较差,而且其使用也更容易出错。

你可能感兴趣的:(volatile关键字总结)