volatile关键字是如何起作用的?

关键字volatile是Java虚拟机提供的最轻量级的同步机制,但是在平时的项目里面,遇到需要多线程的时候更多地使用的是synchronized关键字来进行同步。个人而言,更多的原因是对volatile关键字的机制不了解导致的。

 

Java内存模型对volatile专门定义了一些特殊的访问规则,当一个变量定义为volatile之后便具有了两种特性:

1. 保证此变量对所有线程的可见性,“可见性”指当一条线程修改了这个变量的值,新的值对与其他线程来说是立即得知的。

2. 禁止指令重排序优化。

 

接下来将对上述两个方面分别介绍:

 

普通变量的值在县城之间传递均需要通过主内存来完成,例如,线程A修改一个普通变量的值,然后向内存进行会写,另外一条线程B在线程A回写完之后再从主内存中进行读取操作,新变量值才会对线程B可见。

尽管volatile定义的变量对所有的线程都是可见的,但是并不能说明volatile定义的变量的运算在并发下就是安全的。

(在各个线程的工作内存中,volatile变量也可以存在不一致的情况,但是由于每次使用之前都已经刷新了,执行引擎看不到不一致的情况,所以便认为不存在不一致的情况)

导致不安全的原因其实还是Java运算是非原子操作。所谓原子操作是指操作的执行不会被线程的调度给打断

可以看这个例子:

public class VolatileTest {
    public static volatile int race = 0;
    
    public static void increase(){
        race++;
    }
    
    private static final int THREAD_COUNT = 20;

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREAD_COUNT];
        for(int i = 0; i < THREAD_COUNT; i++){
            threads[i] = new Thread(new Runnable() {
                
                @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);
    }
}

20个线程,每个线程会对race变量进行1000次自增操作,即race++。输出的结果应该是20000,但是会发现每次运行的结果都不一样,而且都是一个小于20000的数。

导致的原因是race++操作并不是一个原子操作,尽管看来它只有一句话,但是在编译时并不是这样的。用javap命名进行反编译,同时输出附加信息:

increase()方法的执行一共是四条字节码指令完成的,熟悉字节码命令的可以看出着四步的操作,

当getstatic指令将race的值取到操作栈的时候,volatile保证了race的值是正确的,但是在执行后面的指令的时候,其它的线程可能已经把race的值修改了。

即使编译出来的只有一条字节码指令,但也不意味这是一个原子操作。

由于volatile变量智能保证可见性,依然需要synchronized和java.util.concurrent中的原子类来保证线程的安全。

 

下面这段代码就展示了一个很好的volatile的使用场景:

volatile Boolean shutdownRequset;

public
void shutdown() { shutdownRequset = true; } public void doWork() { while(!shutdownRequest){ ... } }

当shutdown()方法被调用的时候,能保证所用的doWork()方法都停下来。

 

——————————————————————————————————————————————————————————————————————

 

接下来是第二个方面的用途,普通变量仅仅会保证在该方法执行过程中所有依赖赋值结果的地方都能获得正确的结果,而不能保证变量赋值的操作顺序与程序代码的执行顺序一致。

如果定义的变量没有被volatile修饰,那么就有可能由于指令重排的优化而导致执行顺序的颠倒。

如下面这段代码:

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();
    }
}

通过JIT编译之后就会多执行一个“lock”操作,这个操作相当于一个内存屏障,指重排序之后,不能讲屏障之后的操作放到屏障之前。

 

不过说到底并不能说volatile有什么执行的迅速的特点,但其开销是比锁低的,,唯一需要看的是volatile是否满足使用场景。

posted @ 2017-11-29 21:39  冬日降临  阅读(1918)  评论(0编辑  收藏  举报