嘻哈說:設計模式之單例模式
首先,請您欣賞單例模式的原創歌曲 。
嘻哈說:單例模式 作曲:懶人 作詞:懶人 Rapper:懶人 某個類只有一個例項 並自行例項化向整個系統提供這個例項 需要私有構造方法毋庸置疑 自行例項化各有各的依據 提供單一例項則大體一致 餓漢靜態變數初始化例項 懶漢初始為空 獲取例項為空才建立一次 方法加上鎖弄成執行緒安全的例子 DCL雙重檢查鎖兩次判空加鎖讓併發不是難事 建立物件並不是原子操作因為處理器亂序 volatile的關鍵字開始用武之地 靜態內部類中有一個單例物件的靜態的例項 列舉天生單例 容器管理多個單例 複製程式碼
ofollow,noindex"> 試聽請點選這裡
閒來無事聽聽曲,知識已填腦中去;
學習複習新方式,頭戴耳機不小覷。
番茄課堂,學習也要酷。
2、定義
在Java設計模式中,單例模式相對來說算是比較簡單的一種建立型模式。
什麼是建立型模式?
建立型模式是設計模式的一種分類。
設計模式可以分為三類:建立型模式、結構型模式、行為型模式。
建立型模式:提供了一種在建立物件的同時隱藏建立邏輯的方式,而不是使用 new 運算子直接例項化物件。
結構型模式:關注類和物件的組合,用繼承的概念來組合介面和定義組合物件獲得新功能的方式。
行為型模式:關注物件之間的通訊。
我們來看一下單例模式的定義。
確保某一個類只有一個例項 ,而且自行例項化並向整個系統提供這個例項 。
也就是,保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點 。
單例模式在懶人眼中就是,注孤生,悲慘世界 。
3、特性
從定義中,我們可以分析出一些特性來:
單例類只能有一個例項。
確保某一個類只有一個例項,must be 呀。
單例類必須自行建立自己的唯一的例項。
自行例項化。
單例類必須給所有其他物件提供這一例項。 向整個系統提供這個例項。
記憶體中會長期持有單例例項,如果不是對所有物件提供訪問,例如只對包內類提供訪問許可權,存在的意義就不大了。
4、套路
怎樣確保某一個類只有一個例項?
套路1:私有化空構造方法,避免多處例項化。
套路2:自行例項化,保證例項化在記憶體中只存在一份。
套路3:提供公有靜態getInstance()方法,並將單一的例項返回。
套路1與套路3是固定的套路,基本不會有變。
套路2則有很多靈活的實現方式,只要保證只例項化一次就是可以的。
OK,那我開始擼程式碼。
5、程式碼
1、餓漢模式
package com.fanqiekt.singleton; /** * 餓漢單例模式 * * @author 番茄課堂-懶人 */ public class EHanSingleton { private static EHanSingleton sInstance = new EHanSingleton(); //私有化空構造方法 private EHanSingleton() {} //靜態方法返回單例類物件 public static EHanSingleton getInstance() { return sInstance; } //其他業務方法 public void otherMethods(){ System.out.println("餓漢模式的其他方法"); } } 複製程式碼
套路1:私有化空構造方法。
套路2:自行例項化,保證例項化在記憶體中只存在一份
實現方式:靜態例項變數的初始化 。
實現原理:類載入時就會初始化單例物件,並且只初始化一次。
套路3:提供公有靜態getInstance()方法,並將單一的例項返回。
為什麼叫餓漢?
因為餓漢很餓,需要儘早初始化來餵飽自己。
從執行緒安全,優缺點總結一下。
執行緒安全:利用類載入器的機制,肯定是執行緒安全 的。
為什麼這麼說呢?
ClassLoader的loadClass方法在載入類的時候使用了synchronized關鍵字。
優點:類載入時會初始化單例物件,首次呼叫速度變快。
缺點:類載入時會初始化單例物件,容易產生垃圾。
2、懶漢模式
package com.fanqiekt.singleton; /** * 懶漢模式 * * @author 番茄課堂-懶人 */ public class LazySingleton { private static LazySingleton sInstance; //私有化空構造方法 private LazySingleton() {} //靜態方法返回單例類物件 public static LazySingleton getInstance() { //懶載入 if(sInstance == null) { sInstance = new LazySingleton(); } return sInstance; } //其他業務方法 public void otherMethods(){ System.out.println("懶漢模式的其他方法"); } } 複製程式碼
套路1:私有化空構造方法。
套路2:自行例項化,保證例項化在記憶體中只存在一份
實現方式:getInstance()裡進行例項判空 。
實現原理:為空則建立例項;不為空,則直接返回例項。
套路3:提供公有靜態getInstance()方法,並將單一的例項返回。
為什麼叫懶漢?
因為懶漢懶惰,懶得初始化,用到了才開始初始化。
執行緒安全嗎?
很明顯,不是執行緒安全 的,因為getInstance()方法沒有做任何的同步處理。
怎麼辦?
給getInstance()加鎖。
//靜態方法返回單例類物件,加鎖 public static synchronized LazySingleton getInstance() { //懶載入 if(sInstance == null) { sInstance = new LazySingleton(); } return sInstance; } 複製程式碼
這樣就變成執行緒安全 的懶漢模式了。
懶漢模式有什麼優缺點呢?
優點:第一次使用時才會初始化,節省資源
缺點:第一次使用時需要進行初始化,所以會變慢。給getInstance()加鎖後,getInstance()呼叫也會變慢。
那有沒有辦法可以去掉getInstance()鎖後還執行緒安全呢?
3、DCL
package com.fanqiekt.singleton; /** * Double Check Lock 單例 * * @author 番茄課堂-懶人 */ public class DCLSingleton { private static DCLSingleton sInstance; //私有化空構造方法 private DCLSingleton() {} //靜態方法返回單例類物件 public static DCLSingleton getInstance() { //兩次判空 if(sInstance == null) { synchronized(DCLSingleton.class) { if(sInstance == null) { sInstance = new DCLSingleton(); return sInstance; } } } return sInstance; } //其他業務方法 public void otherMethods(){ System.out.println("DCL模式的其他方法"); } } 複製程式碼
與懶漢模式的區別在於:
去掉getInstance()方法上的鎖,在方法內部例項為空後再進行加鎖。
好處:只有當例項沒有初始化的情況下才會同步鎖,避免了給getInstance()整個方法加鎖的情況。
dcl的全稱是Double Check Lock,雙重檢查 鎖。所謂的雙重檢查就是兩次判空。
為什麼要進行第二次判空,這不是脫褲子放屁,多此一舉嘛。
可能覺得它只是個屁,但其實是竄稀,所以,脫褲子也是有必要的。
有這樣一種情況,執行緒1、2同時判斷第一次為空,在加鎖的地方的阻塞了,如果沒有第二次判空,那麼執行緒1執行完畢後執行緒2就會再次執行,這樣就初始化了兩次,就存在問題了。
兩次判空後,DCL就安全多了,一般不會存在問題。但當併發量特別大的時候,還是會存在風險的。
在哪裡呢?
sInstance = new DCLSingleton()這裡。
是不是很奇怪,這句很普通的建立例項的語句怎麼會有風險。
情況是這樣的:
sInstance = new DCLSingleton()並不是一個原子操作,它轉換成了多條彙編指令,大致做了3件事情:
第一步:分配記憶體。
第二步:呼叫構造方法初始化。
第三步:將sInstanc物件指向分配空間。
由於Java編譯器允許處理器亂序執行,所以這三步順序不定,如果依次執行肯定沒問題,但如果執行完第一步和第三步後,其他的執行緒使用sInstanc就會報錯。
那如何解決呢?
這裡就需要用到關鍵字volatile了。
volatile有什麼用呢?
第一個:實現可見性。
什麼意思呢?
在當前的Java記憶體模型下,執行緒可以把變數儲存在本地記憶體(比如機器的暫存器)中,而不是直接在主存中進行讀寫。
這就可能造成一個執行緒在主存中修改了一個變數的值,而另外一個執行緒還繼續使用它在暫存器中的變數值的拷貝,造成資料的不一致。
volatile在這個時候就派上用場了。
讀volatile:每當子執行緒某一語句要用到volatile變數時,都會從主執行緒重新拷貝一份,這樣就保證子執行緒的會跟主執行緒的一致。
寫volatile: 每當子執行緒某一語句要寫volatile變數時,都會在讀完後同步到主執行緒去,這樣就保證主執行緒的變數及時更新。
第二個:防止處理器亂序執行。
volatile變數初始化的時候,就只能第一步、第二步、第三步這樣的順序執行了。
所以我們可以把sInstance的變數宣告的程式碼更改下。
private volatile static DCLSingleton sInstance; 複製程式碼
不過,由於使用volatile遮蔽掉了JVM中必要的程式碼優化,所以在效率上比較低,因此一定在必要時才使用此關鍵字。
感覺實現起來有點複雜,那有沒有一樣優秀還更簡單點的單例模式?
4、靜態內部類
package com.fanqiekt.singleton; /** * 靜態內部類單例模式 * * @author 番茄課堂-懶人 */ public class StaticSingleton { //私有靜態單例物件 private StaticSingleton() {} //靜態方法返回單例類物件 public static StaticSingleton getInstance() { return SingleHolder.INSTANCE; } //單例類中存在一個靜態內部類 private static class SingleHolder { //靜態類中存在靜態單例宣告與初始化 private static final StaticSingleton INSTANCE = new StaticSingleton(); } //其他業務方法 public void otherMethods(){ System.out.println("靜態內部類的其他方法"); } } 複製程式碼
套路1:私有化空構造方法。
套路2:自行例項化,保證例項化在記憶體中只存在一份
實現方式:宣告一個靜態內部類,靜態內部類中有個單例物件的靜態例項,getInstance()返回靜態內部類的靜態單例物件 。
實現原理:內部類不會在其外部類被載入的時候被載入,只有當內部類被使用的時候才會被使用。這樣就避免了類載入的時候就被初始化,屬於懶載入。
靜態內部類中的靜態變數是通過類載入器初始化的,也就是在記憶體中是唯一的,保證了單例。
執行緒安全:利用了類載入器的機制,肯執行緒安全 。
靜態內部類簡單,執行緒安全,懶載入,所以,強烈推薦 。
還有一個大家可能想象不到的實現方式,那就是列舉。
5、列舉
package com.fanqiekt.singleton; /** * 列舉單例模式 * * @Author: 番茄課堂-懶人 */ public enum EnumSingleton { INSTANCE; //其他業務方法 public void otherMethods(){ System.out.println("列舉模式的其他方法"); } } 複製程式碼
列舉的特點:
保證只有一個例項。
執行緒安全。
自由序列化。
可以說列舉就是一個天生的單例,而且還可以自由序列化,反序列化後也是單例的。
而上邊幾種單例方式反序列化後是會重新再生成物件的,這就是列舉的強大之處。 那列舉的原理是什麼呢?
我們可以看一下生成的列舉反編譯一下,我在這裡只貼上下核心部分。
public final class EnumSingleton extends Enum{ private EnumSingleton(){} static { INSTANCE = new EnumSingleton(); } } 複製程式碼
Enum就是一個普通的類,它繼承自java.lang.Enum類。所以,列舉具有類的所有功能。
他的實現方式優點類似於餓漢模式。
而且,程式碼還做了一些其他的事情,例如:重寫了readResolve方法並將單一例項返回,因此反序列化也會返回同一個例項。
6、容器
package com.fanqiekt.singleton; import java.util.HashMap; import java.util.Map; /** * 容器單例模式 * * @Author: 番茄課堂-懶人 */ public class SingletonManager { private static Map<String, Object> objectMap = new HashMap<>(); //私有化空構造方法 private SingletonManager(){} //將單例的物件註冊到容器中 public static void registerService(String key, Object instance){ if(!objectMap.containsKey(key)){ objectMap.put(key, instance); } } //從容器中獲得單例物件 public static Object getService(String key){ return objectMap.get(key); } } 複製程式碼
實現方式:一個靜態的Map,一個將物件放到map的方法,一個獲取map中物件的方法 。
實現原理:根據key存物件,如果map中已經存在key,則不放入map;不存在key,則放入map,這樣可以保證每個key對應的物件為單一例項。
容器單例的最大好處是,可以管理多個單例。
Android原始碼中就用到了這種方式,通過Context獲取系統級別的服務(context.getSystemService(key))。