【知識積累】通過單例模式學習volatile關鍵字

一、先看一段代碼

/**
 * 單例模式的實現
 */
public class Singleton {

    private static Singleton singleton;

    private Singleton() {
    }

    public static Singleton getSingleton(){
        if (null == singleton){
            synchronized (Singleton.class){
                if (null == singleton){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

上例就是著名的單例模式的雙檢索實現。

在單線程情況下,並不會有什麼問題,但是在多線程情況下,就會有問題。

會有什麼問題呢?

在多線程環境下,一個線程在執行到第一次檢查後,singleton不爲空,但是此時的singleton還沒有完成初始化,因爲singleton = new Singleton();這一步是分爲三步去完成的。

1、分配對象內存空間

2、初始化對象

3、設置instance指向剛分配的內存地址,此時instance!=null

假如此時發生重排序了,順序就變成如下的情況:

1、分配對象內存空間

2、設置instance指向剛分配的內存地址,此時instance!=null

3、初始化對象

爲什麼會發生這種情況?什麼是重排序?一臉懵逼。。。

因爲2和3不存在數據依賴的關係,在單線程情況下,不管是在前面還是在後面,都不會影響程序的結果,這種重排序優化是允許的。當多線程情況下,有一個線程訪問時發現instance!=null,但是instance還沒初始化完成,這就造成了線程的安全性問題。

那怎麼解決呢?

很簡單,加上volatile關鍵字。那這個關鍵字做什麼用的?

volatile:禁止指令重排序

/**
 * 單例模式的實現
 */
public class Singleton {

    private volatile static Singleton singleton;

    private Singleton() {
    }

    public static Singleton getSingleton(){
        if (null == singleton){
            synchronized (Singleton.class){
                if (null == singleton){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

二、volatile如何禁止重排序優化?

概念:內存屏障(Memory Barrier),一個CPU指令。

作用:

1、保證特定操作的執行順序;

2、保證某些變量的內存可見性。

volatile變量正是通過內存屏障實現其在內存中的語義(可見性和禁止重排優化)。

三、volatile的變量爲何立即可見?

當寫一個volatile變量時,JMM會把該線程對應的工作內存中的共享變量刷新到主內存中;
當讀一個volatile變量時,JMM會把該線程對應的工作內存置爲無效,那麼該線程將只能從主內存中重寫讀取共享變量。

四、volatile的內存語義

volatile:JVM提供的輕量級同步機制
volatile關鍵字有如下兩個作用:
1、保證被volatile修飾的共享變量對所有線程總是可見的,即當一個線程修改了一個被volatile修飾的共享變量的值的時候,其他線程立即感知到變動;
2、禁止指令重排序優化。

關於volatile的可見性作用,我們必須意識到被volatile修飾的變量,對所有線程總是立即可見的。對volatile變量的所有寫操作,總是能理解反應到其他線程中,但是對於volatile變量運算操作在多線程環境中並不保證安全性。

public class VolatileDemo {

    public static volatile int i = 0;

    public static void increase(){
        i++;
    }

}

value++並不是一個原子性操作,value++是先讀取值,然後再寫回一個新值,相當於原來的值加上1,分兩步來完成。如果第二個線程在第一個線程讀取舊址和寫回新值期間,讀取value的閾值,那麼第二個線程就會與第一個線程一起看到同一個值,並執行相同的加1操作,也就引發了線程安全問題。因此對於increase方法必須使用synchronized修飾,以便保證線程安全。需要補充,並且注意的是,synchronized關鍵字解決的是執行控制的問題,它會阻止其他線程獲取當前對象的監控鎖,這就使得當前對象中被synchronized關鍵字保護的代碼塊無法被其他線程訪問,也就無法併發執行。

public class VolatileDemo {

    public static volatile int i = 0;

    public synchronized static void increase(){
        i++;
    }

}

在increase方法前面添加了synchronized,更重要的是synchronized還會創建一個內存屏障,內存屏障指令保證了所有CPU結果都會直接刷到主存中,從而保證了操作的內存可見性,同時也使得先獲得這個鎖的線程的所有操作都happens-before於隨後獲得這個鎖的所有操作。

一旦使用synchronized修飾方法之後,由於synchronized本身也具有與volatile相同的特性(即可見性),因此在這樣的情況下,就完全可以省去volatile修飾變量

public class VolatileDemo {

    public static int i = 0;

    public synchronized static void increase(){
        i++;
    }

}

五、volatile和synchronize的區別?

1、volatile本質是在告訴JVM當前變量在寄存器(工作內存)中的值是不確定的,需要從主存中讀取;synchronized則是鎖定當前變量,只有當前線程可以訪問該變量,其他線程被阻塞住直到該線程完成變量操作爲止。
2、vlotaile僅能使用在變量級別;synchronized則可以使用變量、方法和類級別。
3、volatile僅能實現變量的修改可見性,不能保證原子性;而synchronized則可以保證變量修改的可見性和原子性。
4、volatile不會造成線程的阻塞;synchronized可能會造成線程的阻塞。
5、volatile標記的變量不會被編譯器優化;synchronized標記的變量可以被編譯器優化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章