【趣味設計模式系列】之【單例模式】
阿新 • • 發佈:2020-02-14
1. 簡介
單例模式(Singleton):確保某一個類只有一個例項,而且自行例項化並向整個系統提供這個例項。
2. 圖解
類圖如下:
3. 案例實現
單例特點:
- 外部類不能隨便對單例類建立,故單例的
構造方法必須為private
,在類的內部自行例項化; - 提供一個
public方法入口
,作為唯一呼叫單例類的途徑得到例項。
3.1 餓漢式
package com.wzj.singleton; /** * @Author: wzj * @Date: 2020/2/13 12:41 * @Desc: 餓漢式 * 類載入到記憶體後,就例項化一個單例,JVM保證執行緒安全 * 唯一缺點:不管用到與否,類裝載時就完成例項化 */ public class HungrySingleton { private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton(); /** * 私有構造方法,只有本類才能呼叫 */ private HungrySingleton() { } public static HungrySingleton getInstance() { return HUNGRY_SINGLETON; } public static void main(String[] args) { HungrySingleton h1 = HungrySingleton.getInstance(); HungrySingleton h2 = HungrySingleton.getInstance(); System.out.println(h1 == h2); } }
執行結果:
true
- 分析:類載入到記憶體,就例項化一個單例,通過
final
的靜態變數保證唯一例項。 - 優點:執行緒安全。
- 缺點:不管是否用到,類裝載時就完成例項化。
3.2 懶漢式
package com.wzj.singleton; /** * @Author: wzj * @Date: 2020/2/13 12:50 * @Desc: 懶漢式-執行緒不安全 */ public class LazySingleton { private static LazySingleton LAZY_SINGLETON; private LazySingleton() { } public static LazySingleton getInstance() { if (null == LAZY_SINGLETON) { //睡眠1毫秒,在new物件前,增加被其他執行緒打斷的機會,保證能被多個執行緒執行 try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } LAZY_SINGLETON = new LazySingleton(); } return LAZY_SINGLETON; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> System.out.println(LAZY_SINGLETON.getInstance().hashCode()) ).start(); } } }
執行結果
1338303845
1276043710
874014640
2116194883
835777993
527405373
38860249
772510047
484927678
1375039115
.
.
.
- 分析:懶載入在呼叫
getInstace
方法的時候建立例項,通過100個執行緒測試發現其hashcode
並不相等,並不是單例。 - 優點:改善了餓漢式中例項不用也載入的弊端。
- 缺點:引入了新的問題,執行緒不安全,並不能保證單例
3.3 synchronized修飾方法單例
package com.wzj.singleton; /** * @Author: wzj * @Date: 2020/2/13 13:50 * @Desc: 懶漢式-執行緒安全 * 通過synchronized解決,但獲取鎖帶來效能開銷,效率下降 */ public class ThreadSafeSingleton { private static ThreadSafeSingleton LAZY_SINGLETON; private ThreadSafeSingleton() { } public static synchronized ThreadSafeSingleton getInstance() { if (null == LAZY_SINGLETON) { //睡眠1毫秒,在new物件前,增加被其他執行緒打斷的機會,保證能被多個執行緒執行 try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } LAZY_SINGLETON = new ThreadSafeSingleton(); } return LAZY_SINGLETON; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> System.out.println(LAZY_SINGLETON.getInstance().hashCode()) ).start(); } } }
執行結果
2003664945
2003664945
2003664945
2003664945
2003664945
2003664945
2003664945
2003664945
.
.
.
- 分析:通過
synchronized
關鍵字保證執行緒安全。 - 優點:執行緒安全,保證單例。
- 缺點:方法上加鎖導致效能開銷。
3.4 synchronized修飾程式碼塊的單例
package com.wzj.singleton;
/**
* @Author: wzj
* @Date: 2020/2/13 13:50
* @Desc: 懶漢式-執行緒不安全
* 試圖通過減小同步程式碼塊的方式提高效率,帶來了執行緒不安全
*/
public class ThreadUnSafeSingleton {
private static ThreadUnSafeSingleton LAZY_SINGLETON;
private ThreadUnSafeSingleton() {
}
public static ThreadUnSafeSingleton getInstance() {
if (null == LAZY_SINGLETON) {
//試圖通過減小同步程式碼塊的方式提高效率,帶來了執行緒不安全
synchronized (ThreadUnSafeSingleton.class) {
//睡眠1毫秒,在new物件前,增加被其他執行緒打斷的機會,保證能被多個執行緒執行
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
LAZY_SINGLETON = new ThreadUnSafeSingleton();
}
}
return LAZY_SINGLETON;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() ->
System.out.println(LAZY_SINGLETON.getInstance().hashCode())
).start();
}
}
}
結果:
1276043710
1276043710
1276043710
1276043710
1276043710
1276043710
2116194883
2116194883
2116194883
2116194883
.
.
.
- 分析:試圖通過減小同步程式碼塊的方式提高效率,帶來了執行緒不安全。
- 優點:減小了加鎖的範圍,提高了效能。
- 缺點:執行緒不安全,結果顯示不能保證單例。
3.5 雙重檢測安全單例
package com.wzj.singleton;
/**
* @Author: wzj
* @Date: 2020/2/13 13:50
* @Desc: 雙重檢測下執行緒安全
*
*/
public class DoubleCheckedThreadSafeSingleton {
private volatile static DoubleCheckedThreadSafeSingleton LAZY_SINGLETON;
private DoubleCheckedThreadSafeSingleton() {
}
public static DoubleCheckedThreadSafeSingleton getInstance() {
if (null == LAZY_SINGLETON) {
//試圖通過減小同步程式碼塊的方式提高效率,帶來了執行緒不安全
synchronized (DoubleCheckedThreadSafeSingleton.class) {
if (null == LAZY_SINGLETON) {
//睡眠1毫秒,在new物件前,增加被其他執行緒打斷的機會,保證能被多個執行緒執行
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
LAZY_SINGLETON = new DoubleCheckedThreadSafeSingleton();
}
}
}
return LAZY_SINGLETON;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() ->
System.out.println(LAZY_SINGLETON.getInstance().hashCode())
).start();
}
}
}
執行結果
527405373
527405373
527405373
527405373
527405373
527405373
527405373
527405373
.
.
.
- 分析:第一個判空語句
if (null == LAZY_SINGLETON)
,用來檢測如果記憶體中有單例生成以後,永不進入下面的程式碼,直接走return
語句返回已有的單例,第二個判空語句,保證當前執行緒拿到鎖的前後,記憶體中都沒有單例,才執行建立單例操作,防止中途被其他執行緒建立單例,進而重複建立;volatile
關鍵字保證在執行語句LAZY_SINGLETON = new DoubleCheckedThreadSafeSingleton()
時,可以分解為如下的3行虛擬碼。
memory = allocate(); // 1:分配物件的記憶體空間
ctorInstance(memory); // 2:初始化物件
instance = memory; // 3:設定instance指向剛分配的記憶體地址
上面3行虛擬碼中的2和3之間,可能會被重排序(在一些JIT編譯器上,這種重排序是真實發生的)。2和3之間重排序之後的執行時序如下。
memory = allocate(); // 1:分配物件的記憶體空間
instance = memory; // 3:設定instance指向剛分配的記憶體地址
// 注意,此時物件還沒有被初始化!
ctorInstance(memory); // 2:初始化物件
上面的程式碼在編譯器執行時,可能會出現重排序 從1-2-3 排序為1-3-2,如果發生重排序,另一個併發執行的執行緒B就有可能在判斷instance不為null。執行緒B接下來將訪問instance所引用的物件,但此時這個物件可能還沒有被A執行緒初始化!
- 優點:保證單例與執行緒安全。
- 缺點:增加了程式碼的複雜度。
3.6 內部靜態類單例
package com.wzj.singleton;
/**
* @Author: wzj
* @Date: 2020/2/13 15:45
* @Desc: 靜態內部類方式
* JVM保證單例
* 載入外部類時不會載入內部類,這樣可以實現懶載入
*/
public class InnerStaticClassSingleton {
private InnerStaticClassSingleton() {
}
//內部靜態類
private static class InnerStaticClassSingletonHolder {
private static final InnerStaticClassSingleton INSTANCE = new InnerStaticClassSingleton();
}
public static InnerStaticClassSingleton getInstance() {
return InnerStaticClassSingletonHolder.INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() ->
System.out.println(InnerStaticClassSingleton.getInstance().hashCode())
).start();
}
}
}
執行結果:
772510047
772510047
772510047
772510047
772510047
772510047
772510047
772510047
772510047
772510047
.
.
.
- 分析:因為虛擬機器載入類的時候只加載一次,並且載入外部類時不會載入內部類,只有呼叫getInstance方法的時候,內部類才被載入,所以內部類的靜態變數也只加載一次,JVM保證了單例與執行緒安全。
- 優點:執行緒安全。
- 缺點:無。
3.7 列舉單例
package com.wzj.singleton;
/**
* @Author: wzj
* @Date: 2020/2/13 16:05
* @Desc: 列舉實現,既可以保證執行緒安全,還可以防止反序列化。
*/
public enum EnumSingleton {
INSTANCE;
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(EnumSingleton.INSTANCE.hashCode());
}).start();
}
}
}
- 優點:執行緒安全,同時保證不被反序列化,因為列舉型別沒有構造方法,不能反序列化後建立物件。
- 缺點:寫法優點怪異。
4. 框架原始碼分析
以下原始碼分析基於Spring5.0.6 RELEASE。DefaultSingletonBeanRegistry.class
部分原始碼如下.
@Nullable
public Object getSingleton(String beanName) {
return this.getSingleton(beanName, true);
}
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 從單例快取容器中載入 bean
Object singletonObject = this.singletonObjects.get(beanName);
// 單例快取容器中的 bean 為空,且當前 bean 正在建立
if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
// 加鎖
synchronized(this.singletonObjects) {
// 從 earlySingletonObjects 容器中獲取
singletonObject = this.earlySingletonObjects.get(beanName);
// earlySingletonObjects容器中沒有,且允許提前建立
if (singletonObject == null && allowEarlyReference) {
// 從 singletonFactories 中獲取對應的 ObjectFactory
ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
// ObjectFactory 不為空,則建立 bean
if (singletonFactory != null) {
// 獲取 bean
singletonObject = singletonFactory.getObject();
// 新增 bean 到 earlySingletonObjects 中
this.earlySingletonObjects.put(beanName, singletonObject);
// 從 singletonFactories 中移除對應的 ObjectFactory
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
- 第一步,從
singletonObjects
中,獲取 Bean 物件。 - 第二步,若為空且當前
bean
正在建立中,則從earlySingletonObjects
中獲取Bean
物件。 - 第三步,若為空且允許提前建立,則從
singletonFactories
中獲取相應的ObjectFactory
物件。若不為空,則呼叫其ObjectFactory
的getObject(String name)
方法,建立Bean
物件,然後將其加入到earlySingletonObjects
,然後從singletonFactories
刪除。
由此可見,Spring
在建立單例bean
的時候,採用的是雙重檢測加鎖機制建立bean
的。
5. 單例與靜態方法的比較
- 單例支援延遲載入,靜態類第一次載入就初始化;
- 單例常駐記憶體,除非
JVM
退出,靜態方法中的物件,會隨著靜態方法執行完被釋放,gc
回收。
6. 應用場景
- 資料庫連線池,因為頻繁建立或者關閉資料庫連線,損耗效能非常大,因為何用單例模式來維護,就可以大大降低這種損耗;
- 執行緒池,因為執行緒是一種稀缺資源,頻繁建立執行緒,會導系統開銷增大,執行緒之間的頻繁切換也導致效能下降,由統一的執行緒池管理執行緒;
- 開發中常用的配置工具類,因為配置類是共享的資源;
- 日誌應用,因為日誌屬於工享檔案,一直處於開啟狀態,因為只能有一個例項去操作,否則內容不好追加;
- Windows的工作管理員,多次開啟只會彈出一個對話方塊,確保系統由一個工作管理員管理;
- 網站的計數器,如果多個,計數難以同步;
綜上,單例應用在共享資源上,要麼方便管理,要麼節約效能,避免不必要的效能開銷。
7. 總結
單例的具體寫法,需要結合場景與業務要求,確認是否支援執行緒安全,是否支援延遲載入,單例比較簡單的寫法是餓漢式,唯一的不足是不支援懶載入,還有靜態內部類;比較完美的寫法是雙重檢測加鎖,雖然寫法複雜,但支援延遲載入,執行緒安全,也是Spring原始碼使用的方式