溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

確保線程安全的單例模式怎么寫

發(fā)布時(shí)間:2021-06-23 11:48:26 來源:億速云 閱讀:127 作者:chen 欄目:大數(shù)據(jù)

本篇內(nèi)容主要講解“確保線程安全的單例模式怎么寫”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“確保線程安全的單例模式怎么寫”吧!

如何正確地寫出單例模式

單例模式算是設(shè)計(jì)模式中最容易理解,也是最容易手寫代碼的模式了吧。但是其中的坑卻不少,所以也常作為面試題來考。本文主要對幾種單例寫法的整理,并分析其優(yōu)缺點(diǎn)。很多都是一些老生常談的問題,但如果你不知道如何創(chuàng)建一個(gè)線程安全的單例,不知道什么是雙檢鎖,那這篇文章可能會(huì)幫助到你。

1.懶加載 線程不安全

當(dāng)被問到要實(shí)現(xiàn)一個(gè)單例模式時(shí),很多人的第一反應(yīng)是寫出如下的代碼,包括教科書上也是這樣教我們的。

public class Singleton {
    private static Singleton uniqueInstance;
    private Singleton (){}

    public static Singleton getInstance() {
     if (uniqueInstance == null) {
         uniqueInstance = new Singleton();
     }
     return uniqueInstance;
    }
}

這段代碼簡單明了,而且使用了懶加載模式,但是卻存在致命的問題。當(dāng)有多個(gè)線程并行調(diào)用 getInstance() 的時(shí)候,就會(huì)創(chuàng)建多個(gè)實(shí)例。也就是說在多線程下不能正常工作。

2.懶加載 線程安全

為了解決上面的問題,最簡單的方法是將整個(gè) getInstance() 方法設(shè)為同步(synchronized)。

public static synchronized Singleton getInstance() {
    if (uniqueInstance == null) {
        uniqueInstance = new Singleton();
    }
    return uniqueInstance;
}

雖然做到了線程安全,并且解決了多實(shí)例的問題,但是它并不高效。因?yàn)樵谌魏螘r(shí)候只能有一個(gè)線程調(diào)用 getInstance() 方法。但是同步操作只需要在第一次調(diào)用時(shí)才被需要,即第一次創(chuàng)建單例實(shí)例對象時(shí)。這就引出了雙重檢驗(yàn)鎖。

3.雙重檢查加鎖 線程安全

雙重檢驗(yàn)加鎖模式(double checked locking pattern),是一種使用同步塊加鎖的方法。程序員稱其為雙重檢查鎖,因?yàn)闀?huì)有兩次檢查 uniqueInstance == null,一次是在同步塊外,一次是在同步塊內(nèi)。為什么在同步塊內(nèi)還要再檢驗(yàn)一次?因?yàn)榭赡軙?huì)有多個(gè)線程一起進(jìn)入同步塊外的 if,如果在同步塊內(nèi)不進(jìn)行二次檢驗(yàn)的話就會(huì)生成多個(gè)實(shí)例了。

public static Singleton getSingleton() {
    if (uniqueInstance == null) {                         //Single Checked
        synchronized (Singleton.class) {
            if (uniqueInstance == null) {                 //Double Checked
                uniqueInstance = new Singleton();
            }
        }
    }
    return uniqueInstance;
}

這段代碼看起來很完美,很可惜,它是有問題。主要在于uniqueInstance = new Singleton()這句,這并非是一個(gè)原子操作,事實(shí)上在 JVM 中這句話大概做了下面 3 件事情。

  1. 給 uniqueInstance 分配內(nèi)存

  2. 調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量

  3. 將uniqueInstance對象指向分配的內(nèi)存空間(執(zhí)行完這步 uniqueInstance 就為非 null 了)

但是在 JVM 的即時(shí)編譯器中存在指令重排序的優(yōu)化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執(zhí)行完畢、2 未執(zhí)行之前,被線程二搶占了,這時(shí)uniqueInstance已經(jīng)是非 null 了(但卻沒有初始化),所以線程二會(huì)直接返回 uniqueInstance,然后使用,然后順理成章地報(bào)錯(cuò)。

我們只需要將 uniqueInstance 變量聲明成 volatile 就可以了。

public class Singleton {
    private volatile static Singleton uniqueInstance; //聲明成 volatile
    private Singleton (){}

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

有些人認(rèn)為使用 volatile 的原因是可見性,也就是可以保證線程在本地不會(huì)存有 uniqueInstance 的副本,每次都是去主內(nèi)存中讀取。但其實(shí)是不對的。使用 volatile 的主要原因是其另一個(gè)特性:禁止指令重排序優(yōu)化。也就是說,在 volatile 變量的賦值操作后面會(huì)有一個(gè)內(nèi)存屏障(生成的匯編代碼上),讀操作不會(huì)被重排序到內(nèi)存屏障之前。比如上面的例子,取操作必須在執(zhí)行完 1-2-3 之后或者 1-3-2 之后,不存在執(zhí)行到 1-3 然后取到值的情況。從「先行發(fā)生原則」的角度理解的話,就是對于一個(gè) volatile 變量的寫操作都先行發(fā)生于后面對這個(gè)變量的讀操作(這里的“后面”是時(shí)間上的先后順序)。

但是特別注意在 Java 5 以前的版本使用了 volatile 的雙檢鎖還是有問題的。其原因是 Java 5 以前的 JMM (Java 內(nèi)存模型)是存在缺陷的,即使將變量聲明成 volatile 也不能完全避免重排序,主要是 volatile 變量前后的代碼仍然存在重排序問題。這個(gè) volatile 屏蔽重排序的問題在 Java 5 中才得以修復(fù),所以在這之后才可以放心使用 volatile。

相信你不會(huì)喜歡這種復(fù)雜又隱含問題的方式,當(dāng)然我們有更好的實(shí)現(xiàn)線程安全的單例模式的辦法。

4.急加載 static final field 線程安全

這種方法非常簡單,因?yàn)閱卫膶?shí)例被聲明成 static 和 final 變量了,在第一次加載類到內(nèi)存中時(shí)就會(huì)初始化,所以創(chuàng)建實(shí)例本身是線程安全的。

public class Singleton{
    //類加載時(shí)就初始化
    private static final Singleton uniqueInstance = new Singleton();
    
    private Singleton(){}

    public static Singleton getInstance(){
        return uniqueInstance;
    }
}

這種寫法如果完美的話,就沒必要在啰嗦那么多雙檢鎖的問題了。缺點(diǎn)是它不是一種懶加載模式(lazy initialization),單例會(huì)在加載類后一開始就被初始化,即使客戶端沒有調(diào)用 getInstance()方法。餓漢式的創(chuàng)建方式在一些場景中將無法使用:譬如 Singleton 實(shí)例的創(chuàng)建是依賴參數(shù)或者配置文件的,在 getInstance() 之前必須調(diào)用某個(gè)方法設(shè)置參數(shù)給它,那樣這種單例寫法就無法使用了。

5.靜態(tài)內(nèi)部類 static nested class 線程安全

我比較傾向于使用靜態(tài)內(nèi)部類的方法,這種方法也是《Effective Java》上所推薦的。

public class Singleton {  
    private static class SingletonHolder {  
        private static final Singleton uniqueInstance = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
        return SingletonHolder.uniqueInstance; 
    }  
}

這種寫法仍然使用JVM本身機(jī)制保證了線程安全問題;由于 SingletonHolder 是私有的,除了 getInstance() 之外沒有辦法訪問它,因此它是懶加載的;同時(shí)讀取實(shí)例的時(shí)候不會(huì)進(jìn)行同步,沒有性能缺陷;也不依賴 JDK 版本。

枚舉 Enum 線程安全

用枚舉寫單例實(shí)在太簡單了!這也是它最大的優(yōu)點(diǎn)。下面這段代碼就是聲明枚舉實(shí)例的通常做法。

public enum EasySingleton{
    INSTANCE;
}

我們可以通過EasySingleton.INSTANCE來訪問實(shí)例,這比調(diào)用getInstance()方法簡單多了。創(chuàng)建枚舉默認(rèn)就是線程安全的,所以不需要擔(dān)心double checked locking,而且還能防止反序列化導(dǎo)致重新創(chuàng)建新的對象。但是還是很少看到有人這樣寫,可能是因?yàn)椴惶煜ぐ伞?/p>

總結(jié)

一般來說,單例模式有五種寫法:懶加載、急加載、雙重檢查加鎖鎖、靜態(tài)內(nèi)部類、枚舉。上述所說都是線程安全的實(shí)現(xiàn),文章開頭給出的第一種方法不算正確的寫法。

就我個(gè)人而言,一般情況下直接使用急加載就好了,如果明確要求要懶加載(lazy initialization)會(huì)傾向于使用靜態(tài)內(nèi)部類,如果涉及到反序列化創(chuàng)建對象時(shí)會(huì)試著使用枚舉的方式來實(shí)現(xiàn)單例。

到此,相信大家對“確保線程安全的單例模式怎么寫”有了更深的了解,不妨來實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI