java開發:單例模式:爲什麼要加雙重驗校?爲什麼要加volatile?

//final類不可繼承
final public class Single {
    //使用volatile修飾變量
    private static  Single single = null;
    public static Single create() {
        //第一次驗校
        if (single == null) {
            //同步代碼塊(類鎖)
            synchronized (Single.class) {
                //第一次驗校
                if (single == null) {
                    single = new Single();
                }
            }
        }

        return single;
    }

    /**
     * 私有構造函數,外部訪問不了
     */
    private Single() {
    }
}


這是一個典型的雙重鎖單例模式,在很多單例源碼中經常可以看見。

1.爲什麼要進行第一次判空

我們知道單例模式只有第一次執行create()方法的時候纔會走synchronized 中的代碼,後面再次訪問的時候直接返回single 對象。如果說我們沒有第一次驗校,每一個線程都要走synchronized 中的代碼,而每一次線程都要去拿到同步鎖才能執行。在多線程的情況下每一個線程要拿到single 對象都要排隊等待同步鎖釋放。因此第一次驗校作用就是爲了提高程序的效率。

2.爲什麼要進行第二次判空

舉個例子:假如現在沒有第二次驗校,線程A執行到第一次驗校那裏,它判斷到single ==null。此時它的資源被線程B搶佔了,B執行程序,進入同步代碼塊創建對象,然後釋放同步鎖,此時線程A又拿到了資源也拿到了同步鎖,然後執行同步代碼塊,因爲之前線程A它判斷到single ==null,因此它會直接創建新的對象。所以就違反了我們設計的最終目的。

3.變量爲什麼要加volatile關鍵字

在上面例子中volatile保證代碼指令不會被重排序,首先我們得先了解什麼是volatile關鍵字與及它的特性。

volatile關鍵字的特性:

volatile具有可見性、有序性,不具備原子性。
注意,volatile不具備原子性,這是volatile與java中的synchronized、java.util.concurrent.locks.Lock最大的功能差異,這一點在面試中也是非常容易問到的點。
原子性:不可打斷的操作,要麼成功要麼失敗。例如基本數據類型的讀寫操作都是屬於原子性int a = 10這一過程就是原子性(即可以看成當一個線程執行int a = 10這句代碼時其他線程是處於等待獲取cpu資源的狀態,因爲線程併發是通過cpu調度交替執行,並不是真正的並行執行),但是像a++這樣的運算操作就是非原子性,因爲a++虛擬機要運行三個指令:讀取a,a+1,a賦值,這個過程是允許打斷的。
volatile具有可見性是指當一個線程對變量進行原子操作的時候,另外的線程能立即獲取到最新的數據。
java如何實現可見性需要了解Java的內存模型:

在這裏插入圖片描述
Java內存模型(即Java Memory Model,簡稱JMM)本身是一種抽象的概念,並不真實存在。它描述了不同線程訪問共享變量的規則。
我們程序所有的共享變量都是放在主內存中,而線程會從主內存拷貝它需要的共享變量到自己的工作內存
線程之間變量的傳遞則需要靠主內存。
Java內存模型規定對共享變量的操作只能在自己工作內存進行,即我讀一個共享變量時需要先從工作內存找。寫的時候也需要修改工作內存的變量後再push到主內存。

在多線程併發的情況下會發生什麼問題?
假如主內存中有個共享變量 int a = 10,線程A和線程B的工作內存都有這個變量的副本。假如線程A在自己工作內存中修改了a的值,int a = 20,此時線程A還未來得及push到主內存中線程B就已經讀取了a的值,線程B讀取到的值是10,因此就造成了數據的混亂。
而java的volatile保證了共享變量的可見性:volatile修飾的變量當線程在工作內存修改後會立馬push到主內存中,同時會把工作內存的變量設置爲禁止讀取,因此訪問這個變量的時候不能從自己的工作內存訪問,必須要去主內存中取。

還是舉上面的例子:變量a被volatile修飾 volatile int a = 10,線程A修改了a的值立馬push到主存中,線程B訪問的時候到主存訪問就可以得到最新的值。

其次volatile還可以禁止虛擬機對指令進行重排序。指令重排作用是虛擬機在不影響單線程程序執行結果的前提下對指令重新排序,提高程序運行效率。但是重排序在多線程併發的情況下也是容易出現問題的。

在上訴單例模式中volatile保證了虛擬機執行字節碼的時候指令不會重排序。
single = new Single() 在我們看來就是一句話操作而已,但在虛擬機看來它一共分爲了幾個指令操作:
1.爲對象分配內存空間
2.初始化對象
3.將引用指向對象的內存空間地址
虛擬機執行的時候不一定是按順序123的執行,也有可能是132。這是虛擬機的重排序引起的,單線程情況下是沒有什麼bug的,最終都會創建出對象,只是先後順序不同。
但是在上面例子中會出現這麼一種情況:
假如線程A執行 single = new Single()虛擬機是按132排序執行,當執行到3的時候single 引用已經不爲空。此時若線程B執行到第一次驗校處(第一次驗校不在同步代碼中,因此所有線程隨時都可以訪問),它判斷 single ==null 得到false,直接返回single對象。但是此時single對象還沒初始化完成,因此很有可能就會發生bug。

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