什麼是單例模式
兩種類型
- 懶漢式:在真正需要使用對象時纔去創建該單例類對象
- 餓漢式:在類加載時已經創建好該單例對象,等待被程序使用
其實比較好理解的
餓漢式
餓漢式在類加載時已經創建好該對象,在程序調用時直接返回該單例對象即可。我們目前可以簡單認爲在程序啓動時,這個單例對象就已經創建好了。
public class Singleton { //餓漢式 private static Singleton singleton=new Singleton(); private Singleton() { } public Singleton getInstance() { return singleton; } }
構造方法私有,這樣就不能在外部new出對象,只能使用getInstance獲取對象。
優缺點:
懶漢式
public class Singleton { //懶漢式 private static Singleton singleton; private Singleton() {} public Singleton getInstance() { if(singleton==null) { singleton=new Singleton(); } return singleton; } }
最基礎的寫法,類加載的時候沒有實例化,使用時才new
但這種寫法會出現併發問題
如果兩個線程同時判斷singleton爲空,那麼它們都會去實例化一個Singleton對象,這就變成雙例了。所以,我們要解決的是線程安全問題。
加上同步方法或者同步代碼塊
public static synchronized Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; } // 或者 public static Singleton getInstance() { synchronized(Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } return singleton; }
但這兩種方式都不好,我們要鎖住的是 創建對象這一過程
獲取對象這個過程沒必要加鎖
public static Singleton getInstance() { if (singleton == null) { // 線程A和線程B同時看到singleton = null,如果不爲null,則直接返回singleton synchronized(Singleton.class) { // 線程A或線程B獲得該鎖進行初始化 if (singleton == null) { // 其中一個線程進入該分支,另外一個線程則不會進入該分支 singleton = new Singleton(); } } } return singleton; }
這種寫法相對較好:
- 之所以進入判斷後還要再判斷一次,是因爲可能其他線程已經把它實例化了
- 鎖住的爲什麼是 Singleton.class 鎖住一個類
指令重排問題
創建一個對象,在JVM中會經過三步:
(1)爲singleton分配內存空間
(2)初始化singleton對象
(3)將singleton指向分配好的內存空間
指令重排序是指:JVM在保證最終結果正確的情況下,可以不按照程序編碼的順序執行語句,儘可能提高程序的性能
在這三步中,第2、3步有可能會發生指令重排現象,創建對象的順序變爲1-3-2,會導致多個線程獲取對象時,有可能線程A創建對象的過程中,執行了1、3步驟,線程B判斷singleton已經不爲空,獲取到未初始化的singleton對象,就會報NPE異常。使用volatile關鍵字可以防止指令重排序。
public class Singleton { private static volatile Singleton singleton; private Singleton(){} public static Singleton getInstance() { if (singleton == null) { // 線程A和線程B同時看到singleton = null,如果不爲null,則直接返回singleton synchronized(Singleton.class) { // 線程A或線程B獲得該鎖進行初始化 if (singleton == null) { // 其中一個線程進入該分支,另外一個線程則不會進入該分支 singleton = new Singleton(); } } } return singleton; } }
最終代碼,把對象用volatile修飾
單例模式使用場景
創建的一個對象需要消耗的資源過多,比如 I/O 與數據庫的連接等。