1、嘻哈說
首先,請您欣賞單例模式的原創歌曲。
嘻哈說:單例模式
作曲:懶人
作詞:懶人
Rapper:懶人
某個類只有一個例項
並自行例項化向整個系統提供這個例項
需要私有構造方法毋庸置疑
自行例項化各有各的依據
提供單一例項則大體一致
餓漢靜態變數初始化例項
懶漢初始為空
獲取例項為空才建立一次
方法加上鎖弄成執行緒安全的例子
DCL雙重檢查鎖兩次判空加鎖讓併發不是難事
建立物件並不是原子操作因為處理器亂序
volatile的關鍵字開始用武之地
靜態內部類中有一個單例物件的靜態的例項
列舉天生單例
容器管理多個單例
複製程式碼
閒來無事聽聽曲,知識已填腦中去;
學習複習新方式,頭戴耳機不小覷。
番茄課堂,學習也要酷。
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))。
6、END
單例模式實現的方式雖然有很多,但都是為了讓某一個類只有一個例項。
今天就先說到這裡,下次是建造者模式,感謝大家。