在面試中咱們常常會被問到:「你熟悉單例模式嗎?請手寫一個單例模式的實現?單例模式的應用有哪些……」。有關單例模式的問題比比皆是,在面試中也是很是常見的。html
所謂單例模式就是確保一個類只有一個實例,並對外提供該實例的全局訪問點。java
類圖以下: git
解讀:public class LazySingleton {
// 構造函數私有化
private LazySingleton(){
System.out.println("當前線程名稱: " + Thread.currentThread().getName() + "\t 我是構造方法...");
}
// 私有的靜態變量
private static LazySingleton lazySingleton;
// 提供靜態的全局訪問點
public static LazySingleton getSingleton() {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
System.out.println(lazySingleton); // 打印當前對象的惟一標識
return lazySingleton;
}
}
複製代碼
public class TestSingletons {
public static void main(String[] args) {
// 單線程場景下,直接調用
LazySingleton.getSingleton();
LazySingleton.getSingleton();
LazySingleton.getSingleton();
LazySingleton.getSingleton();
// 多線程場景
/*for (int i = 0; i < 10; i++) { new Thread(() -> { LazySingleton.getSingleton(); }, String.valueOf(i)).start(); }*/
}
}
複製代碼
當前線程名稱: main 我是構造方法...
singleton.LazySingleton@4554617c
singleton.LazySingleton@4554617c
singleton.LazySingleton@4554617c
singleton.LazySingleton@4554617c
複製代碼
4554617c
,說明單線程場景下是沒有問題的
。當前線程名稱: 2 我是構造方法...
當前線程名稱: 8 我是構造方法...
當前線程名稱: 4 我是構造方法...
singleton.LazySingleton@ae526cf
當前線程名稱: 6 我是構造方法...
當前線程名稱: 0 我是構造方法...
singleton.LazySingleton@134f6cee
當前線程名稱: 3 我是構造方法...
當前線程名稱: 9 我是構造方法...
singleton.LazySingleton@6e154e44
當前線程名稱: 5 我是構造方法...
當前線程名稱: 7 我是構造方法...
當前線程名稱: 1 我是構造方法...
singleton.LazySingleton@2fd04fd1
singleton.LazySingleton@67c084e5
singleton.LazySingleton@47e3e4b5
singleton.LazySingleton@1b9c704e
singleton.LazySingleton@21279f82
singleton.LazySingleton@2ceb2de
singleton.LazySingleton@14550b42
複製代碼
沒法保證線程安全的
。在上一個實現方式的基礎上加以改進,以求保證在多線程條件下能夠達到線程安全的目的。基於這種思路,能夠得出懶漢式的另一種實現方式——線程安全的實現方式。github
public class LazySafeSingleton {
// 構造方法私有化
private LazySafeSingleton(){
System.out.println("當前線程名稱: " + Thread.currentThread().getName() + "\t 我是構造方法...");
}
// 私有的靜態變量
private static LazySafeSingleton lazySafeSingleton;
// 提供同步的靜態全局訪問點
public synchronized static LazySafeSingleton getSingleton() {
if (lazySafeSingleton == null) {
lazySafeSingleton = new LazySafeSingleton();
}
System.out.println(lazySafeSingleton); // 打印當前對象的惟一標識
return lazySafeSingleton;
}
}
複製代碼
該方式與第一種方式只有一點區別:在提供的全局訪問點,即獲取實例對象的公共方法加了同步鎖,保證同一時刻,只能由一個線程訪問 getSingleton() 方法。面試
延伸閱讀: java synchronized詳解安全
public class TestSingletons {
public static void main(String[] args) {
// 單線程場景
LazySafeSingleton.getSingleton();
LazySafeSingleton.getSingleton();
LazySafeSingleton.getSingleton();
LazySafeSingleton.getSingleton();
// 多線程場景
/*for (int i = 0; i < 10; i++) { new Thread(() -> { LazySafeSingleton.getSingleton(); }, String.valueOf(i)).start(); }*/
}
}
複製代碼
當前線程名稱: main 我是構造方法...
singleton.LazySafeSingleton@4554617c
singleton.LazySafeSingleton@4554617c
singleton.LazySafeSingleton@4554617c
singleton.LazySafeSingleton@4554617c
複製代碼
當前線程名稱: 0 我是構造方法...
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
複製代碼
分析上一個實現 getSingleton() 方法上加了 synchronized 關鍵字修飾,雖然能保證同一時刻只能由一個線程訪問,保證了多線程場景下的一致性,可是這也會帶了另一個問題:併發性下降。因此,接着對懶漢式-線程安全
進行改進。bash
public class DCLSingleton {
// 構造方法私有化
private DCLSingleton(){
System.out.println("當前線程名稱: " + Thread.currentThread().getName() + "\t 我是構造方法...");
}
// 靜態私有變量
private volatile static DCLSingleton dclSingleton;
// 提供靜態全局訪問點
public static DCLSingleton getDclSingleton() {
if (dclSingleton == null) {
synchronized (DCLSingleton.class) {
if (dclSingleton == null) {
dclSingleton = new DCLSingleton();
}
}
}
System.out.println(dclSingleton); // 打印當前對象的惟一標識
return dclSingleton;
}
}
複製代碼
對比上一個實現方式,會發現有兩處區別:多線程
volatile
關鍵字。 緣由是:dclSingleton = new DCLSingleton();
編譯成字節碼後分爲三個步驟:1. 爲 dclSingleton 分配內存空間
2. 初始化 dclSingleton
3. 將 dclSingleton 執行分配的內存地址
複製代碼
因爲jvm具備指令重排的特性,在多線程環境下就可能會出現一個線程獲取到的實例還未被初始化的狀況
。例如:線程T1執行了1和3,此時線程T2調用 getDclSingleton() 方法後發現 dclSingleton 不爲空,所以會返回 dclSingleton,可是此時 dclSingleton 還未被初始化。所以在聲明靜態私有變量時添加volatile關鍵字保證jvm沒法進行指令重排,從而解決上述問題。併發
if (dclSingleton == null) {
synchronized (DCLSingleton.class) {
dclSingleton = new DCLSingleton();
}
}
複製代碼
在只有一個 if 的代碼中,多線程條件下,假設線程T1和線程T2同時進入 dclSingleton == null 語句,接着T1或T2其中的一個線程會執行 dclSingleton = new DCLSingleton(); ,在執行結束以後會釋放鎖,另一個線程也會再次執行 dclSingleton = new DCLSingleton(); 語句,這致使構造函數執行了兩次,所以在同步代碼塊中,須要再次對 dclSingleton 是否爲空進行判斷。jvm
public class TestSingletons {
public static void main(String[] args) {
// 單線程
DCLSingleton.getDclSingleton();
DCLSingleton.getDclSingleton();
DCLSingleton.getDclSingleton();
DCLSingleton.getDclSingleton();
// 多線程
/*for (int i = 0; i < 10; i++) { new Thread(() -> { LazySafeSingleton.getSingleton(); }, String.valueOf(i)).start(); }*/
}
}
複製代碼
當前線程名稱: main 我是構造方法...
singleton.DCLSingleton@4554617c
singleton.DCLSingleton@4554617c
singleton.DCLSingleton@4554617c
singleton.DCLSingleton@4554617c
複製代碼
當前線程名稱: 0 我是構造方法...
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
複製代碼
懶漢式與餓漢式的最主要區別在於,懶漢式的靜態私有變量爲空,在使用時進行構造;而餓漢式則在加載時就已經構造好了,即在使用前即已經構造完畢。這種方式會形成必定的資源浪費。
public class HungrySingleton {
// 構造方法私有化
private HungrySingleton() {
System.out.println("當前線程名稱: " + Thread.currentThread().getName() + "\t 我是構造方法...");
}
// 靜態私有變量
private static HungrySingleton hungrySingleton = new HungrySingleton();
// 提供靜態全局訪問點
public static HungrySingleton getSingleton() {
System.out.println(hungrySingleton); // 打印當前對象的惟一標識
return hungrySingleton;
}
}
複製代碼
測試方法與上面的懶漢式的建立方式一致,會發現不論是單線程環境下仍是多線程條件下,這種方式都是隻會構造一次。
對 餓漢式-線程安全 的實現方式進行改進,能夠對 建立實例對象 和 使用實例對象 兩個步驟進行解耦,即實現使用時在進行建立。靜態內部類徹底符合。
public class InnerClazzSingleton {
// 私有化構造方法
private InnerClazzSingleton(){}
// 靜態內部類,保證使用時才加載
private static class InnerClassSingletonHolder {
private static final InnerClazzSingleton SINGLETON = new InnerClazzSingleton();
}
// 提供靜態全局訪問點
public static InnerClazzSingleton getInstance() {
return InnerClassSingletonHolder.SINGLETON;
}
}
複製代碼
這種方式利用了靜態內部類在使用時纔會進行加載的特性。即調用 getInstance() 方法時 InnerClassSingletonHolder 纔會被加載,此時會初始化 SINGLETON 實例,而且也能保證只被初始化一次。這種方式不只具備餓漢式的線程安全的特色,又具備延遲初始化節省系統資源的特色。 測試方法略。
延伸閱讀:朝花夕拾——Java靜態內部類加載
public enum EnumSingleton {
INSTANCE;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
複製代碼
不一樣的實現方式 | 特色 |
---|---|
1. 懶漢式-線程不安全 | 線程不安全;具備延遲加載解決資源的特色; |
2. 懶漢式-線程安全 | 對1進行改造——在全局訪問點處添加同步機制。能保證線程安全,雖然說多線程能保證一致性,可是沒法保證併發性 |
3. 雙重鎖校驗式-線程安全 | 對2進行改造,以求提升併發性,使用volatile 修飾靜態實例變量,同步前和同步後均須要校驗實例變量是否爲空。線程安全 |
4. 餓漢式-線程安全 | 對1進行改造,在使用前即會建立實例變量。全局只會建立一次,所以能保證線程安全,可是會形成資源浪費的問題 |
5. 靜態內部類-線程安全 | 對4進行改造,利用靜態內部類使用時纔會加載的特性將 實例變量的使用權 和 構造權 解耦。線程安全 |
6. 枚舉類-線程安全 | 線程安全,多適用於單元素場景 |
延伸閱讀: 單例模式