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是否滿足使用場景。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM