1. 程式人生 > >如何正確地寫出單例模式(懶漢式和餓漢式寫法)

如何正確地寫出單例模式(懶漢式和餓漢式寫法)

本文轉自大神:伍翀 原文連結

單例模式算是設計模式中最容易理解,也是最容易手寫程式碼的模式了吧。但是其中的坑卻不少,所以也常作為面試題來考。本文主要對幾種單例寫法的整理,並分析其優缺點。很多都是一些老生常談的問題,但如果你不知道如何建立一個執行緒安全的單例,不知道什麼是雙檢鎖,那這篇文章可能會幫助到你。

懶漢式,執行緒不安全

當被問到要實現一個單例模式時,很多人的第一反應是寫出如下的程式碼,包括教科書上也是這樣教我們的。

public class Singleton {
    private static Singleton instance;
    private Singleton (){}
    public static Singleton getInstance() {
     if (instance == null) {
         instance = new Singleton();
     }
     return instance;
    }
}

這段程式碼簡單明瞭,而且使用了懶載入模式,但是卻存在致命的問題。當有多個執行緒並行呼叫 getInstance() 的時候,就會建立多個例項。也就是說在多執行緒下不能正常工作。

懶漢式,執行緒安全

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

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

雖然做到了執行緒安全,並且解決了多例項的問題,但是它並不高效。因為在任何時候只能有一個執行緒呼叫 getInstance() 方法。但是同步操作只需要在第一次呼叫時才被需要,即第一次建立單例例項物件時。這就引出了雙重檢驗鎖。

雙重檢驗鎖

雙重檢驗鎖模式(double checked locking pattern),是一種使用同步塊加鎖的方法。程式設計師稱其為雙重檢查鎖,因為會有兩次檢查 instance == null,一次是在同步塊外,一次是在同步塊內。為什麼在同步塊內還要再檢驗一次?因為可能會有多個執行緒一起進入同步塊外的 if,如果在同步塊內不進行二次檢驗的話就會生成多個例項了。

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

這段程式碼看起來很完美,很可惜,它是有問題。主要在於instance = new Singleton()這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。

  1. 給 instance 分配記憶體
  2. 呼叫 Singleton 的建構函式來初始化成員變數
  3. 將instance物件指向分配的記憶體空間(執行完這步 instance 就為非 null 了)

但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,被執行緒二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以執行緒二會直接返回 instance,然後使用,然後順理成章地報錯。

我們只需要將 instance 變數宣告成 volatile 就可以了。

public class Singleton {
    private volatile static Singleton instance; //宣告成 volatile
    private Singleton (){}
    public static Singleton getSingleton() {
        if (instance == null) {                         
            synchronized (Singleton.class) {
                if (instance == null) {       
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

有些人認為使用 volatile 的原因是可見性,也就是可以保證執行緒在本地不會存有 instance 的副本,每次都是去主記憶體中讀取。但其實是不對的。使用 volatile 的主要原因是其另一個特性:禁止指令重排序優化。也就是說,在 volatile 變數的賦值操作後面會有一個記憶體屏障(生成的彙編程式碼上),讀操作不會被重排序到記憶體屏障之前。比如上面的例子,取操作必須在執行完 1-2-3 之後或者 1-3-2 之後,不存在執行到 1-3 然後取到值的情況。從「先行發生原則」的角度理解的話,就是對於一個 volatile 變數的寫操作都先行發生於後面對這個變數的讀操作(這裡的“後面”是時間上的先後順序)。

但是特別注意在 Java 5 以前的版本使用了 volatile 的雙檢鎖還是有問題的。其原因是 Java 5 以前的 JMM (Java 記憶體模型)是存在缺陷的,即時將變數宣告成 volatile 也不能完全避免重排序,主要是 volatile 變數前後的程式碼仍然存在重排序問題。這個 volatile 遮蔽重排序的問題在 Java 5 中才得以修復,所以在這之後才可以放心使用 volatile。

相信你不會喜歡這種複雜又隱含問題的方式,當然我們有更好的實現執行緒安全的單例模式的辦法。

餓漢式 static final field

這種方法非常簡單,因為單例的例項被宣告成 static 和 final 變量了,在第一次載入類到記憶體中時就會初始化,所以建立例項本身是執行緒安全的。

public class Singleton{
    //類載入時就初始化
    private static final Singleton instance = new Singleton();
    
    private Singleton(){}
    public static Singleton getInstance(){
        return instance;
    }
}

這種寫法如果完美的話,就沒必要在囉嗦那麼多雙檢鎖的問題了。缺點是它不是一種懶載入模式(lazy initialization),單例會在載入類後一開始就被初始化,即使客戶端沒有呼叫 getInstance()方法。餓漢式的建立方式在一些場景中將無法使用:譬如 Singleton 例項的建立是依賴引數或者配置檔案的,在 getInstance() 之前必須呼叫某個方法設定引數給它,那樣這種單例寫法就無法使用了。

靜態內部類 static nested class

我比較傾向於使用靜態內部類的方法,這種方法也是《Effective Java》上所推薦的。

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

這種寫法仍然使用JVM本身機制保證了執行緒安全問題;由於 SingletonHolder 是私有的,除了 getInstance() 之外沒有辦法訪問它,因此它是懶漢式的;同時讀取例項的時候不會進行同步,沒有效能缺陷;也不依賴 JDK 版本。

列舉 Enum

用列舉寫單例實在太簡單了!這也是它最大的優點。下面這段程式碼就是宣告列舉例項的通常做法。

public enum EasySingleton{
    INSTANCE;
}

我們可以通過EasySingleton.INSTANCE來訪問例項,這比呼叫getInstance()方法簡單多了。建立列舉預設就是執行緒安全的,所以不需要擔心double checked locking,而且還能防止反序列化導致重新建立新的物件。但是還是很少看到有人這樣寫,可能是因為不太熟悉吧。

總結

一般來說,單例模式有五種寫法:懶漢、餓漢、雙重檢驗鎖、靜態內部類、列舉。上述所說都是執行緒安全的實現,文章開頭給出的第一種方法不算正確的寫法。

就我個人而言,一般情況下直接使用餓漢式就好了,如果明確要求要懶載入(lazy initialization)會傾向於使用靜態內部類,如果涉及到反序列化建立物件時會試著使用列舉的方式來實現單例。











相關推薦

如何正確模式懶漢式寫法

本文轉自大神:伍翀 原文連結 單例模式算是設計模式中最容易理解,也是最容易手寫程式碼的模式了吧。但是其中的坑卻不少,所以也常作為面試題來考。本文主要對幾種單例寫法的整理,並分析其優缺點。很多都是一些老生常談的問題,但如果你不知道如何建立一個執行緒安全的單例,不知道什

模式懶漢式區別

單例模式 所謂單例模式,就是保證類在記憶體中只有一個物件 而如何保證類在記憶體中只有一個物件? 思考一下,我們平時在例項化類的物件時,基本都是通過new 的方式來例項化一個物件,其實說白了,就是呼叫了需要例項化類的預設的構造方法,所以為了保證類只有一個物件,我們需要將類

設計模式模式懶漢式

設計模式: 一些人總結出來用來解決特定問題的固定的解決方案。 單例模式 解決一個類在記憶體中只存在一個物件,想要保證物件的唯一。 1 為了避免其他程式過多的建立該類物件。禁止其他程式建立該類物件。 2 為了其他程式可以訪問該類物件,在本類中自定義一個物件。 3 方便其他程

模式懶漢式及如何實現執行緒安全

單例模式有兩種:懶漢式和餓漢式。 1 #include <iostream> 2 3 using namespace std; 4 5 6 // 保證在整個程式執行期間,最多隻能有一個物件例項 7 8 9 // 懶漢式 10 // 1 、建構函式私有化 11

模式懶漢式

單例模式是一個類有且僅有一個例項,並且自行例項化向整個系統提供,常用的有懶漢式和餓漢式。 一、懶漢式:在第一次呼叫的時候才例項化自己。 public class Singleton {

設計模式模式懶漢式

設計模式第一個模式通常是單例模式,是為了防止某個類存在多個物件。 程式碼如下: **singlon.h:** #pragma once #ifndef _SINGLON_H #define _SINGLON_H class singlon { publ

模式懶漢式

懶漢式: public class SingleTon{   private static SingleTon singleTon;   private SingleTon(){}   public static SingleTon getSingleTon(){     if(singleTon

java常用的幾種模式懶漢式、登記

簡單的講,單例模式就是確保某個類只有一個例項,而且自行例項化並向整個系統提供這個例項。 何時用到?執行緒池、快取、日誌物件、對話方塊、顯示卡驅動程式、印表機中都用到,spring中用的最多:Spring Context Factory用的是單例,bean預設都是

java模式懶漢式的區別,雙層鎖

單例就是該類只能返回一個例項。 單例所具備的特點: 1.私有化的建構函式 2.私有的靜態的全域性變數 3.公有的靜態的方法 一般常見到的是3種: 餓漢式(執行緒不安全): public class Singleton1 {       private Singleton

android之模式懶漢式的區別

單例模式:懶漢式和餓漢式    餓漢式:執行緒安全:構造方法私有化:推薦使用          public class Singleton{            private static Si

模式懶漢式

mce private 靜態工廠方法 pri return let class 懶漢 single //懶漢式public class Singleton { private Singleton() { } private static Singleton

【J2EE學習】如何正確模式

單例模式算是設計模式中最容易理解,也是最容易手寫程式碼的模式了吧。但是其中的坑卻不少,所以也常作為面試題來考。本文主要對幾種單例寫法的整理,並分析其優缺點。很多都是一些老生常談的問題,但如果你不知道如何建立一個執行緒安全的單例,不知道什麼是雙檢鎖,那這篇文章可能會幫助到你。

如何正確模式

單例模式算是設計模式中最容易理解,也是最容易手寫程式碼的模式了吧。但是其中的坑卻不少,所以也常作為面試題來考。本文主要對幾種單例寫法的整理,並分析其優缺點。很多都是一些老生常談的問題,但如果你不知道如何建立一個執行緒安全的單例,不知道什麼是雙檢鎖,那這篇文章

Java如何正確模式

單例模式算是設計模式中最容易理解,也是最容易手寫程式碼的模式了吧。但是其中的坑卻不少,所以也常作為面試題來考。本文主要對幾種單例寫法的整理,並分析其優缺點。很多都是一些老生常談的問題,但如果你不知道如何建立一個執行緒安全的單例,不知道什麼是雙檢鎖,那這篇文章可能會幫助到你。 懶漢式,執行緒不安全 當被

C++ 模式懶漢式

應該都知道一個單例模式怎樣去實現: 1、建構函式宣告為private或protect防止被外部函式例項化。 2、提供一個全域性的靜態方法(全域性訪問點)。 3、內部儲存一個private static的類指標儲存唯一的例項,例項的動作由一個public的類方法代勞,該方法也

模式懶漢,

ati turn 還需 sin 有用 只需要 對象 clas main Java中的單例模式一般分為懶漢模式和餓漢模式,懶漢模式只有用得到的時候對象才初始化,餓漢模式無論用得到與否,都先初始化。 懶漢模式在運行的時候獲取對象比較慢(因為類加載時並沒有創建對象實例),但是加載

C++實現一個模式懶漢與

單例模式的特點: 1、一個類只能有一個例項。 2、一個類必須自己建立自己的唯一例項。 3、一個類必須給所有其他物件提供這一例項。 單例模式的實現: 1、將建構函式宣告為private防止被外部

01.JavaGOF23-建立型模式-模式-應用場景__懶漢式

Windows的Task Manager(工作管理員)就是很典型的單例模式 windows的Recycle Bin(回收站)也是典型的單例應用。在整個系統執行過程中,回收站一直維護著僅有的一個例項。 專案中,讀取配置檔案的類,一般也只有一個物件。沒有必要每次使用配置檔案資料,每次new一個物件去讀取。 網站

設計模式模式-懶漢模型模型

什麼是單例模式? 保證一個類只有一個例項,並提供一個訪問它的全域性訪問點。首先,需要保證一個類只有一個例項;在類中,要構造一個例項,就必須呼叫類的建構函式,如此,為了防止在外部呼叫類的建構函式而構造例項,需要將建構函式的訪問許可權標記為protected或pr

Java面試題之在多線程情況下,模式中懶漢會有什麽問題呢?

餓漢模式 問題 之間 static 代碼 clas ava public 餓漢 懶漢模式和餓漢模式: public class Demo { //private static Single single = new Single();//餓漢模式