引
好多碼農寫的最6的模式就是單例模式,其中也包括我。最先接觸的設計模式也就是這個單例模式,后來隨著業(yè)務拓展發(fā)現(xiàn)這個用的最得心應手的模式也有翻車的時候,在這里記錄下解決問題后的收獲。
使用單例應該注意什么
1.創(chuàng)建單例對象資源消耗問題
2.多線程同步
3.反序列化單例對象的保持
第一種
public class SingleA {
private static instance;
private SingleA() {
}
public static SingleA getInstance() {
if (instance == null) {
instance = new SingleA();
}
return instance;
}
}
這個應該就是最好寫的單例了吧,什么多線程,什么資源消耗全都靠邊站,我就是要單例對象~~~所以這種寫法有好多弊端。
第一個注意點,創(chuàng)建單例對象資源消耗問題。這個問題也可以勉強歸結到創(chuàng)建單例的時機問題上面。如果說一個單例對象里面依賴一個線程池對象,數據庫管理對象或者其他資源消耗比較大的對象,這個時候如果我們在程序剛起來的時候就創(chuàng)建的話這樣就會造成耗時較長,內存比較吃緊的問題,如果說這時候創(chuàng)建了單例對象但是并不需要立即使用,那么這就是一個錯誤的時機。所以就可以放在使用他的地方去創(chuàng)建。
第二種
比如說這種單例,什么都不管只要你運行了本項目,那我肯定第一個站出來初始化自己~~
public class SingleB {
private static instance = new SingleB();
private SingleB() {
}
public static SingleB getInstance() {
return instance;
}
}
如果說我們這個單例對象初始化時機并不是這么趕,占用資源較少還好,如果占用資源比較多那就伴隨整個項目生命周期,對項目會是一種負擔。
線程同步問題。我們知道cpu在每個時間片上會來回切換執(zhí)行不同的任務,同理多個線程也會來回切換執(zhí)行我們代碼,這就造成了可能創(chuàng)建出多個對象,所以當我們的代碼涉及到并發(fā)時就需要考慮這個單例是否需要加同步來避免多線程并發(fā)時導致意想不到的錯誤出現(xiàn)。
第三種
public class SingleC {
private static instance;
private SingleC() {
}
public static SingleC getInstance() {
synchronized (SingleC.class) {
if (instance == null) {
instance = new SingleC();
}
}
return instance;
}
}
這里加了個同步,這樣做目的為了在多線程下防止單例對象重復創(chuàng)建,但是這么做其實隱藏著很多問題。如果這個單例對象頻繁被使用,那么每次調用時都會走這個同步,這樣平白增加了我們的運行時間所以可以考慮使用下面這種雙檢驗來減少同步帶來的開銷。按下面這種方式來創(chuàng)建單例對象,只有第一次創(chuàng)建時開銷較大,后續(xù)使用中就會屏蔽掉每次的同步,大大的提高了我們的執(zhí)行效率。就像下面這種。
第四種
public class SingleC {
private volatile static instance;
private SingleC() {
}
public static SingleC getInstance() {
if (instance == null) {
synchronized (SingleC.class) {
if (instance == null) {
instance = new SingleC();
}
}
}
return instance;
}
}
這里我們加了volatile來修飾單例對象,這個關鍵字并不能實現(xiàn)原子操作,他只是告訴所有線程,想要使用我修飾的這個對象,請各位爺(多個線程)到主存中去找,別用自己的。這樣可以在線程并發(fā)不高的情況下滿足我們多線程單例對象的需求,但是凡事也有個意外,也許我們通常寫的多線程情況下的單例也就像上面一樣就可以認定為在多線程中沒有問題了。答案卻是否定的。amazing,原因就是,雖然在new一個對象時候看似是一個指令,但是到了計算機那里會產生至少三個匯編指令去執(zhí)行,而且計算機是允許亂序執(zhí)行的,這樣就會導致在使用這個單例時會發(fā)生一些錯誤。
第五種
所以我們還有更好的解決辦法那就是使用枚舉單例,靜態(tài)初始化單例。由于靜態(tài)初始化單例分為兩種,這里先介紹枚舉單例。
public enum SingleC {
INSTANCE;
/*
*一些單例方法
*
*/
}
使用枚舉單例其中之一的好處是即使多線程高并發(fā),他也能保證線程安全,另外一個好處就是單例對象在序列化和反序列化下還能Hold住自己。
下面來說下靜態(tài)初始化單例。
靜態(tài)初始化單例分為兩種
靜態(tài)初始化的第一種,對應上面第二種單例的寫法這里重復貼下代碼
public class SingleB {
private static instance = new SingleB();
private SingleB() {
}
public static SingleB getInstance() {
return instance;
}
}
當然這種單例的問題在上面已經給了,這里就不說了。
第六種單例寫法
下面我們介紹靜態(tài)初始化單例的第二種,其實也不復雜,就像一個抖機靈。看代碼。
public class SingleD {
private SingleD() {
}
public static SingleD getInstance() {
}
public static class SingleDHolder{
public static SingleD instance = new SingleD();
}
}
這兩種都會完美的避開多線程高并發(fā)的情況下單例錯誤的產生,但是第一種會有一個缺點,那就是他會在加載類的時候創(chuàng)建對象,如果這個單例占用的資源較小可以忽略不計,但是如果他占用資源很多,那么我們就要考慮第二種了,在延遲初始化的情況下還可以保證多線程高并發(fā)下不出錯誤。
第七種
下面介紹下最后一種??赡軙械奖容^無語的一種單例,但是如果閱讀過安卓源碼我們會知道,安卓里面獲取系統(tǒng)服務正是使用的這種單例。所以我們還是有必要了解一下。
public class SingleContainer {
private HashMap<String,Object> containerMap = new HashMap<String,Object>();
private SingleContainer() {
}
public static void putInstance(String key,Object instance) {
if (!containerMap.containsKey(key)) {
containerMap.put(key,instance);
}
}
public static Object getInstance(String key) {
return containerMap.get(key);
}
}
這種就是Android里面保存系統(tǒng)服務的單例,比如他會初始化WindowsManagerService或者AMS等系統(tǒng)服務初始化后保存在這個map里面,如果我們需要這個服務(單例對象)那就從Map中拿,這樣也是一種單例。
以上就是我理解的七種單例的寫法,可以說各有利弊,可以根據我們不同情況來選擇使用。我認為以上幾種可以應對我們大部分的開發(fā)需求了,可能會有更多的寫法,如果后面碰到了再來補充,也歡迎大家告知。