單例模式有五種寫法:懶漢、餓漢、雙重檢驗鎖、靜態內部類、枚舉。_設計模式

來源:互聯網
上載者:User

http://blog.csdn.net/nsw911439370/article/details/50456231


轉 https://biezhi.me/article/how-to-correctly-write-singleton-pattern.html

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

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

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 件事情。 給 instance 分配記憶體 調用 Singleton 的建構函式來初始化成員變數 將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)會傾向於使用靜態內部類,如果涉及到還原序列化建立對象時會試著使用枚舉的方式來實現單例。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.