單例模式
單例模式是開發中常見的設計模式。
設計模式,是我們在編寫代碼時候的一種軟性的規定,也就是說,我們遵守了設計模式,代碼的下限就有了一定的保證。設計模式有很多種,在不同的語言中,也有不同的設計模式,設計模式也可以被認為是對編程語言語法的補充。
單例,即單個實例(對象),某個類在一個進程中,只應該創建出一個實例(原則上不應該創建出多個實例),使用單例模式,可以對我們的代碼進行一個更為嚴格的校驗和檢查。
舉個例子:有時候,代碼中需要管理/持有大量的數據,此時有一個對象就可以了。比如:我需要一個對象管理10G的數據,如果我們不小心創建出多個對象,內存空間就會成倍地增長。
如何保證只有唯一的對象呢?我們可以選擇“君子之約地方式”,寫一個文檔,文檔上約定,每個接手維護代碼的程序員,都不能對這個類創建多個實例(很顯然,這種約定并不靠譜)我們期望讓機器(編譯器)能夠對代碼中的指定類,對創建的實例個數進行檢驗。如果發現創建出了多個實例,就直接編譯報錯,但是Java語法中本身沒有辦法直接約定某個對象能創建出幾個實例,那么就需要程序員使用一些技巧來實現這樣的效果。
實現單例模式的方式有很多種,這里介紹兩種實現方式:餓漢模式和懶漢模式。
1 餓漢模式
代碼如下:
//餓漢模式
//期望這個類只能有唯一的實例(一個進程中)
class Singleton{private static Singleton instance = new Singleton();//在這個類被加載時,就會初始化這個靜態成員,實例創建的時機非常早——餓漢public static Singleton getInstance(){//其他代碼想要使用這個類的實例就需要通過這個方法進行獲取,// 不應該在其他代碼中重新new這個對象而是使用這個方法獲取這個現有的對象return instance;}private Singleton(){//其他代碼就沒法new了}
}
在這個類中,我們創建出了唯一的對象,被static修飾,說明這個變量是類變量,(由類對象所擁有(每個類的類對象只存在一個),在類加載的時候,它就已經被初始化了)。
而將構造方法設為私有,就使得只能在當前類里面創建對象了,其他位置就不能再創建對象了,因此這個instance指向的對象就是唯一的對象。
其他代碼要想使用這個類的實例,就需要通過這個getInstance()方法獲取這個對象,而無法在其他代碼中new一個對象。
上述代碼,稱為”餓漢模式“,是單例模式中的一種簡單的寫法,”餓“形容”非常迫切“,實例在類加載的時候就創建了,創建的時機非常早,相當于程序一啟動,實例就創建了。?
但是,上面的代碼,面對反射,是無能為力的,也就是說,仍然可以通過反射來創建對象,但反射是屬于非常規的編程手段,代碼中隨意使用反射是非常糟糕的。
2 懶漢模式
”懶“這個詞,并不是貶義詞,而是褒義詞。社會能進步,科技能發展,生產效率提高,有很大部分原因都是因為懶。
舉個生活中的例子(不考慮衛生):
假如我每次吃完飯就洗碗,那我每次就需要洗全部的碗;但是如果我每次吃完飯把碗放著,等到下次吃飯的時候再洗,此時,如果我只要用到兩個碗,那我就只需要洗兩個碗就行了,很明顯洗兩個碗要比洗全部碗更加高效。
在計算機中,”懶“的思想就非常有意思,它通常代表著更加高效。
比如有一個非常大的文件(10GB),使用編輯器打開這個文件,如果是按照”餓漢“的方式 ,編輯器就會先把這10GB的數據都加載到內存中,然后再進行統一的展示。(但是加載了這么多數據,用戶還是需要一點一點地看,沒法一下子看完這么多)
如果是按照”懶漢“地方式,編輯器就會只讀取一小部分數據(比如只讀取10KB),把這10KB先展示出來,然后隨著用戶進行翻頁之類的操作,再繼續展示后面的數據。
加載10GB的時間會很長,但是加載10KB卻只是一瞬間的事情……
懶漢模式,區別于餓漢模式,創建實例的時機不一樣了,創建實例的時機會更晚,一直到第一次使用getInstance方法時才會創建實例。
代碼如下(注意:這是一個不完整的代碼,因為還有一些線程安全問題需要解決~~):
//懶漢的方式實現單例模式class SingletonLazy{private static SingletonLazy instance = null;public static SingletonLazy getInstance(){//餓漢模式是在類加載的時候就創建實例了,懶漢則會晚很多,且如果程序用不到這個方法就會省下了if (instance == null) {//如果首次調用就創建實例instance = new SingletonLazy();}}}//不是則返回之前創建的引用return instance;}private SingletonLazy(){}
}
第一行代碼中仍然是先創建一個引用,但是這個引用不指向任何的對象。如果是首次調用getInstance方法,就會進入if條件,創建出對象并且讓當前引用指向該對象。如果是后續調用getInstance方法,由于當前的instance已經不是null了,就會返回我們之前創建的引用了。
這樣設定,仍然可以保證,該類的實例是唯一一個,與此同時,創建實例的時機就不再是程序驅動了,而是當第一次調用getInstance的時候,才會創建。。
而第一次調用getInstance這個操作的執行時機就不確定了,要看程序的實際需求,大概率會比餓漢這種方式要晚一些,甚至有可能整個程序壓根用不到這個方法,也就把創建的操作給省下了。
有的程序,可能是根據一定的條件,來決定是否要進行某個操作,進一步來決定是否要創建實例。?
3 單例模式與線程安全
上面我們介紹的關于單例模式只是一個開始,接下來才是我們多線程的真正關鍵問題。即:上述我們編寫的餓漢模式和懶漢模式,是否是線程安全的?
餓漢模式:
//餓漢模式
//期望這個類只能有唯一的實例(一個進程中)
class Singleton{private static Singleton instance = new Singleton();//在這個類被加載時,就會初始化這個靜態成員,實例創建的時機非常早——餓漢public static Singleton getInstance(){//其他代碼想要使用這個類的實例就需要通過這個方法進行獲取,// 不應該在其他代碼中重新new這個對象而是使用這個方法獲取這個現有的對象return instance;}private Singleton(){//其他代碼就沒法new了}
}
對于餓漢模式來說,getInstance直接返回instance這個實例,這個操作,本質上就是一個讀的操作(多個線程同時讀取同一變量,是不會產生線程安全問題的)。因此,在多線程下,它是線程安全的。
懶漢模式 :
//懶漢的方式實現單例模式class SingletonLazy{private static SingletonLazy instance = null;public static SingletonLazy getInstance(){//餓漢模式是在類加載的時候就創建實例了,懶漢則會晚很多,且如果程序用不到這個方法就會省下了if (instance == null) {//如果首次調用就創建實例instance = new SingletonLazy();}//不是則返回之前創建的引用return instance;}private SingletonLazy(){}
}
再看懶漢模式,在懶漢模式中,代碼中有讀的操作(return instance),又有寫的操作(instance = new SingletonLazy())。?很明顯,這是一個有線程安全問題的代碼!!!
問題1:線程安全問題
因為多線程之間是隨機調度,搶占是執行的,如果t1和 t2 按照下列的順序執行代碼,就會出現問題。
如果是t1和t2按照上述情況操作,就會導致實例被new了兩次,這就不是單例模式了,就會出現bug了!!!
那如何解決當前的代碼bug,使它變為一個線程安全的代碼呢?
加鎖~~
知道要加鎖了?那大家不妨想想:如果我把鎖像如下代碼這樣加下去,是否線程就安全了呢?
class SingletonLazy{private static SingletonLazy instance = null;Object locker = new Object;public static SingletonLazy getInstance(){//餓漢模式是在類加載的時候就創建實例了,懶漢則會晚很多,且如果程序用不到這個方法就會省下了if (instance == null) {//如果首次調用就創建實例sychronized(locker){instance = new SingletonLazy();}}//不是則返回之前創建的引用return instance;}private SingletonLazy(){}
}
答案很顯然:不行!!!因為如上述代碼加鎖仍然會發生剛才那樣的線程不安全的情況。
所以這里如果想要代碼正確執行,需要把if和new兩個操作,打包成一個原子的操作(即加鎖加在if語句的外面)。?
class SingletonLazy{private static SingletonLazy instance = null;Object locker = new Object;public static SingletonLazy getInstance(){//餓漢模式是在類加載的時候就創建實例了,懶漢則會晚很多,且如果程序用不到這個方法就會省下了synchronized(locker){ if (instance == null) {//如果首次調用就創建實例instance = new SingletonLazy();}} //不是則返回之前創建的引用return instance;}private SingletonLazy(){}
}
?
此時因為t1拿到了鎖,t2進入阻塞,等t1執行完畢后(創建完對象后),t2進行判斷,此時因為t1已經創建好了對象,所以t2就只能返回當前對象的引用了。?
多線程的代碼是非常復雜的,代碼稍微變化一點,結論就可能截然不同。千萬不能認為,代碼中加了鎖就一定線程安全,不加鎖就一定線程不安全,具體問題要具體分析,要分析這個代碼在各種調度執行順序下不同的情況,確保每種情況都不會出現bug!!!
?問題2:效率問題
上述代碼還存在的另一個問題是效率問題:試想一下,當你創建完這個單例對象,你每次獲取這個單例對象時(是讀的操作,并不會有線程問題),每次都要去加鎖、解鎖,然后才能返回這個對象。(注意:加鎖、解鎖耗費的空間和時間都是很大的)。
所以為了優化上面的代碼,我們可以再加上一層if,如果instance為null(需要執行寫操作),考慮到線程安全問題,就需要加鎖;如果instance不為null了,就不需要加鎖了。
class SingletonLazy{private static SingletonLazy instance = null;Object locker = new Object;public static SingletonLazy getInstance(){//餓漢模式是在類加載的時候就創建實例了,懶漢則會晚很多,且如果程序用不到這個方法就會省下了if(instance == null){synchronized(locker){ if (instance == null) {//如果首次調用就創建實例instance = new SingletonLazy();}}} //不是則返回之前創建的引用return instance;}private SingletonLazy(){}
}
上面的代碼,有兩重完全相同if判斷條件,但是他們的作用是完全不同的:
第一個if是判斷是否需要加鎖,第二個if是判斷是否要創建對象!!!
巧合的是,兩個if條件相同,但是他們的作用是完全不同的,這樣就實現了雙重校驗鎖。在以后的學習中,還可能出現兩個if條件是相反的情況。
問題3:指令重排序問題
這個代碼還有一點問題需要解決:我們之前在線程安全的原因中講過的:指令重排序問題就在懶漢模式上出現了~~
指令重排序,也是編譯器優化的一種方式。編譯器會在保證邏輯不變的前提下,為了提高程序的效率,調整原有代碼的執行順序。
再舉個生活中的例子:
我媽讓我去超市買東西:西紅柿、雞蛋、黃瓜、茄子。
超市攤位分布圖如下:
如果我按我媽給的順序,那就會走出這樣的路線:?
上述方案雖然也能完成我媽給的任務,但如果我對超市已經足夠熟悉了,我就能夠在保證邏輯不變
的情況下(買到4種菜),調整原有買菜的執行順序,提高買菜效率:?
返回到代碼中:
instance = new SingletonLazy();
?上面這行代碼,可以拆分為三個步驟:
1、申請一段內存空間。
2、調用構造方法,創建出當前實例。
3、把這個內存地址賦給instance這個引用。
上述代碼可以按1、2、3這個順序來執行,但是編譯器也可能會優化成1、3、2這個順序執行。這兩種順序在單線程下都是能夠完成任務的。
1就相當于買了個房子
2相當于裝修房子
3相當于拿到了房子的鑰匙
通過1、2、3得到的房子,拿到的房子已經是裝修好的,稱為“精裝房”;通過1、3、2得到的房子,拿到的房子需要自己裝修,稱為“毛坯房”,我們買房子時,上面的兩種情況都可能發生。
但是,如果在多線程環境下,指令重排序就會引入新問題了。
上述代碼中,由于 t1 線程執行完 1 3 步驟(申請一段內存空間,把內存空間的地址賦給引用變量,但并沒有進行 2 調用構造方法的操作,會導致 instance指向的是一個未被初始化的對象)之后調度走,此時 instance 指向的是一個非 null 的,但是是未初始化的對象,此時 t2 線程判定 instance == null 不成立,就會直接 return,如果 t2 繼續使用 instance 里面的屬性或者方法,就會出現問題,引起代碼的邏輯出現問題。?
那么我們應該如何解決當前問題呢?
volatile關鍵字
之前講過volatile有兩個功能:
1、保證內存可見性:每次訪問變量都必須要重新讀取內存,而不會優化為讀寄存器/緩存。
2、禁止指令重排序:針對被volatile修飾的變量的讀寫操作的相關指令,是不能被重排序的。
懶漢模式的完整代碼:
//經典面試題!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
package Thread;
//懶漢的方式實現單例模式
//線程不安全,它在多線程環境下可能會創建多個實例
class SingletonLazy{//這個引用指向唯一實例,這個引用先初始化為null,而不是立即創建實例
private volatile static SingletonLazy instance = null;//針對這個變量的讀寫操作就不能重排序了
private static Object locker;
//第一次if判定是否要加鎖,第二次if判定是否要創建對象//雙重校驗鎖public static SingletonLazy getInstance(){//餓漢模式是在類加載的時候就創建實例了,懶漢則會晚很多,且如果程序用不到這個方法就會省下了//加鎖效率不高,且容易導致阻塞,所以再加一個判斷提高效率if(instance ==null) {//判斷是否為空,為空再加鎖//不為空,說明是后續的調用就無需加鎖了synchronized (locker) {if (instance == null) {//如果首次調用就創建實例instance = new SingletonLazy();}}}//不是則返回之前創建的引用return instance;}private SingletonLazy(){}
}