【設(shè)計模式】秒懂單例模式
單例模式介紹
概述
單例模式:某一個類在系統(tǒng)中只需要有一個實例對象,而且對象是由這個類自行實例化并提供給系統(tǒng)其它地方使用,這個類稱為單例類。單例模式是GOF 23種設(shè)計模式中最簡單的一種,但同時也是在項目中接觸最多的一種。單例模式屬于一種創(chuàng)建型設(shè)計模式。
使用場景
大家都使用過Windows任務(wù)管理器,正常情況下,無論我們在Windows任務(wù)欄的右鍵菜單上點擊啟動多少次“任務(wù)管理器”,系統(tǒng)始終只能彈出一個任務(wù)管理器窗口。也就是說,在一個Windows系統(tǒng)中,系統(tǒng)只維護(hù)一個任務(wù)管理器。這就是一個典型的單例模式運(yùn)用。
再舉一個例子,網(wǎng)站的計數(shù)器,一般也是采用單例模式實現(xiàn),如果你存在多個計數(shù)器,每一個用戶的訪問都刷新計數(shù)器的值,這樣的話你的實計數(shù)的值是難以同步的。但是如果采用單例模式實現(xiàn)就不會存在這樣的問題,而且還可以避免線程安全問題。同樣多線程的線程池的設(shè)計一般也是采用單例模式,這是由于線程池需要方便對池中的線程進(jìn)行控制。
可以看出,我們在程序中使用單例模式,目的一般是處理資源訪問的沖突,或者從業(yè)務(wù)概念上,有些數(shù)據(jù)在系統(tǒng)中只應(yīng)保存一份,那也比較適合設(shè)計為單例類,比如配置類、全局流水號生成器等。
UML類圖

單例模式實現(xiàn)
單例模式實現(xiàn)要點
單例模式雖然簡單,但是要寫出一個能保證在多線程環(huán)境下也能保證實例唯一性的單例確不是那么簡單,實現(xiàn)一個正確的單例模式有以下幾個要點:
? 1.某個類只能有一個實例,即使是多線程運(yùn)行環(huán)境下;
? 2.單例類的實例一定是單例類自身創(chuàng)建,而不是在單例類外部用其它方式如new方式創(chuàng)建;
? 3.單例類需要提供一個方法向整個系統(tǒng)提供這個實例對象。
單例兩種模式
單例模式分為餓漢模式和懶漢模式,這兩種模式很好理解,懶漢模式的意思就是這個類很懶,只要別人不找它要實例,它都懶得創(chuàng)建。餓漢模式在初始化時,我們就創(chuàng)建了唯一的實例,即便這個實例后面并不會被使用。
下面分別介紹兩種單例模式的寫法。
懶漢式
下面這種寫法的單例是大家最簡單最容易寫出的一種單例寫法,只適用于單線程的系統(tǒng),也就是說它不是線程安全的。
//懶漢式,線程不安全
class Singleton1{
private static Singleton1 instance;
//構(gòu)造函數(shù)定義為私有,防止外部創(chuàng)建實例
private Singleton1(){
}
//系統(tǒng)使用單例的入口
public static Singleton1 getInstance(){
if (null == instance){
instance = new Singleton1();
}
return instance;
}
}針對線程不安全的問題,可以通過獲取實例的方法添加了synchronized來解決,如下:
//懶漢式,線程安全,效率低
class Singleton2{
private static Singleton2 instance;
//構(gòu)造函數(shù)定義為私有,防止外部創(chuàng)建實例
private Singleton2(){
}
//系統(tǒng)使用單例的入口
public staticsynchronizedSingleton2 getInstance(){
if (null == instance){
instance = new Singleton2();
}
return instance;
}
}
這樣一來,確實線程安全了,但是又帶來了另一個問題:程序的性能極大的降低了,高并發(fā)下多個線程去獲取這個實例,現(xiàn)在卻要排隊。
針對性能問題,有同學(xué)想到了減小synchronized的粒度,不加在方法上,而是放在代碼塊中:
//懶漢式,線程不安全
class Singleton3{
private static Singleton3 instance;
//構(gòu)造函數(shù)定義為私有,防止外部創(chuàng)建實例
private Singleton3(){
}
//系統(tǒng)使用單例的入口
public static Singleton3 getInstance(){
if (null == instance){
synchronized(Singleton3.class){
instance = new Singleton3();
}
}
return instance;
}
}
但是,很不幸,如果改成這樣,又變得線程不安全了,我們試著分析一個代碼執(zhí)行的場景:假設(shè)我們有兩個線程 T1與T2并發(fā)訪問getInstance方法。當(dāng)T1執(zhí)行完if (instance == null)且instance為null時,其CUP執(zhí)行時間被T2搶占,所以T1還沒有創(chuàng)建實例。T2也執(zhí)行if (instance == null),此時instance肯定還為null,T2執(zhí)行創(chuàng)建實例的代碼,當(dāng)T1再次獲得CPU執(zhí)行時間后,其從synchronized 處恢復(fù),又會創(chuàng)建一個實例。
那么有沒有一種寫法,可以同時兼顧到效率和線程安全兩方面了,還真有,就是我們下面將要介紹的double-check的方式。
////懶漢式,線程安全,效率還可以
class Singleton4{
//注意加上volatile關(guān)鍵字
private static volatile Singleton4 instance;
//構(gòu)造函數(shù)定義為私有,防止外部創(chuàng)建實例
private Singleton4(){
}
//系統(tǒng)使用單例的入口
public static Singleton4 getInstance(){
//第一次檢查提高訪問性能
if (null == instance){
synchronized(Singleton4.class) {
//第二次檢查為了線程安全
if(instance ==null) {
instance = new Singleton4();
}
}
}
return instance;
}
}
這種單例的寫法做了兩次 if (null == instance)的判斷,因此被稱為double-check的方式。
? 第一次check為了提高訪問性能。因為一旦實例被創(chuàng)建,后面線程的所有的check都為假,不需要執(zhí)行synchronized競爭鎖了。
? 第二次check是為了線程安全,確保多線程環(huán)境下只生成一個實例。
需要注意的是,這種方式,在定義實例時一定需要加上volatile 關(guān)鍵字,禁止虛擬機(jī)指令重排,否則,還是有一定幾率會生成多個實例,關(guān)于volatile 關(guān)鍵字和指令重排的問題這里不過多介紹,后面在多線程安全系列文章中再詳細(xì)介紹。
餓漢式
使用靜態(tài)常量在類加載時候就創(chuàng)建了實例,屬于餓漢模式。其是線程安全的,這一點由JVM來保證。
//餓漢式,線程安全
class Singleton5{
//
private static final Singleton5 INSTANCE = new Singleton5();
//構(gòu)造函數(shù)定義為私有,防止外部創(chuàng)建實例
private Singleton5(){
}
//系統(tǒng)使用單例的入口
public static Singleton5 getInstance(){
return INSTANCE;
}
}
本文源碼地址:
https://github.com/qinlizhong1/javaStudy/tree/master/DesignPattern/src/singleton
本文示例代碼環(huán)境:
操作系統(tǒng):macOs 12.1
JDK版本:12.0.1
maven版本: 3.8.4
推薦閱讀:
完全整理 | 365篇高質(zhì)技術(shù)文章目錄整理
專注服務(wù)器后臺技術(shù)棧知識總結(jié)分享
歡迎關(guān)注交流共同進(jìn)步
