單例設計模式,意味着整個系統中只能存在一個實例,比方說像日志對象這種。我們常說的有餓漢式和懶漢式這兩種模式來創建單例對象,今天就拓展一下思維,多看幾種。
首先我們若是想一個類只有一個對象,那肯定先要私有化構造器,斷了在其它的類中使用構造器創建實例的念頭。其它的類中不能創建,我們就只能在類中自己創建一個私有實例,另外還要提供一個共有的方法使其它對象獲取到實例。所以,第一版出現了。
1 【餓漢式 V1】
在類加載的時候就創建實例
@ThreadSafe
public class SingletonExample2 {
// 私有化構造器
private SingletonExample2(){}
// 提供一個實例
private static SingletonExample2 instance = new SingletonExample2();
// 提供共有的方法返回實例
public static SingletonExample2 getInstance(){
return instance;
}
}
不要忘了在多線程環境中還有關注線程是否安全,我這里都會打上注解,@ThreadSafe 表示線程安全,@NotThreadSafe 表示線程不安全。
上面這種方式就是比較簡單的,也是最容易想到的方式,就有一個缺點,若是不使用這個對象,那就有點浪費資源了,這個對象不一定會被使用,但是我們已經創建好了。
2 【餓漢式 V2】
這種方式是借助於 "靜態代碼塊只會被加載一次" 來實現單例的創建,很簡單,也很好理解,問題和餓漢式一樣,不一定就會使用到這個對象,所以可能會出現浪費資源的情況。
@ThreadSafe
public class SingletonExample6 {
// 私有化構造器
private SingletonExample6(){}
private static SingletonExample6 instance = null;
static {
instance = new SingletonExample6();
}
// 提供共有的方法返回實例
public static SingletonExample6 getInstance(){
return instance;
}
}
3 【懶漢式 V1】
在對象使用的時候才創建實例
@NotThreadSafe
public class SingletonExample1 {
// 私有化構造器
private SingletonExample1(){}
// 提供一個實例
private static SingletonExample1 instance = null;
// 提供共有的方法返回實例
public static SingletonExample1 getInstance(){
if(instance == null){
return new SingletonExample1();
}
return instance;
}
}
這種方式在單線程的時候是沒有問題的,但是在多線程時就會出現問題,假如線程 A 進入 if 之后暫停執行,此時又來一個線程 B 還是可以進入 if 並返回一個實例,此時 A 再次獲得執行時,返回的是另一個實例了。
4 【懶漢式 V2】
在共有方法上添加 synchronized 關鍵字,同步該方法。可行,但是不推薦使用,因為 synchronized 修飾方法之后,在同一時刻只能有一個線程執行該方法,一旦有線程獲得方法,其它線程需要等待,這樣會浪費大量時間,系統運行效率降低。
@ThreadSafe
@NotRecommend
public class SingletonExample3 {
// 私有化構造器
private SingletonExample3(){}
// 提供一個實例
private static SingletonExample3 instance = null;
// 提供共有的方法返回實例
public static synchronized SingletonExample3 getInstance(){
if(instance == null){
return new SingletonExample3();
}
return instance;
}
}
5 【懶漢式 V3】
這種方式使用雙重檢測 + 防止指令重排的方式來保證線程安全,首先需要注意的是在 getInstance 方法中,我們需要雙層檢測並使用同步代碼塊將創建對象的過程同步起來。
@NotThreadSafe
public class SingletonExample4 {
// 私有化構造器
private SingletonExample4(){}
// 提供一個實例
private static SingletonExample4 instance = null;
// 提供共有的方法返回實例
public static SingletonExample4 getInstance(){
// 線程 B 判斷,發現 instance 不為空,直接返回,而實際上 instance 還沒有初始化。
if(instance == null){ // 雙重檢測機制
synchronized (SingletonExample4.class) { // 同步鎖
if(instance == null){
// 線程 A 執行到重排后的指令 3 ,此時 instance 已經有地址值了。但是沒有初始化
return new SingletonExample4(); // 這里是重點!!
}
}
}
return instance;
}
}
因為在 new SingletonExample4() 的過程中,並不是一個原子操作,是可以進一步拆分為:
1、分配對象內存空間
memory = allocate()
2、初始化對象
initInstance()
3、設置 instance 指向剛分配的內存
instance = memory
在多線程的情況下,上面 3 個指令會存在指令重排序的情況。【JVM 和 CPU 指令優化】重排后的結果可能為:
memory = allocate()
instance = memory
initInstance()
此時可能會存在線程 A 在內層 if 執行到指令重排后的第 3 步,但並未初始化,只是存在了地址值,線程 B 在外層 if 判斷時,會直接 return 實例,而這個實例是一個只有地址值而沒有被初始化的實例。
為了防止指令重排帶來的問題呢,我們就可以使用 volatile 關鍵字防止指令重排。這樣就是線程安全的了。只需在上一版的基礎上使用 volatile 修飾 instance 實例即可。
volatile 的語義就是添加內存屏障和防止指令重排,這在前面已經分析過了。
private static volatile SingletonExample4 instance = null;
6 【使用枚舉類實現單例模式】
這是推薦使用的方法,因為它比懶漢式的線程安全更容易保證,比餓漢式的性能高,它只有在調用的時候才實例對象。
@ThreadSafe
@Recommend
public class SingletonSpecial {
private SingletonSpecial(){}
public static SingletonSpecial getInstance(){
return Singleton.INSTANCE.getInstance();
}
private enum Singleton{
INSTANCE;
// public static final Singleton INSTANCE;
private SingletonSpecial singleton;
// JVM 來保證這個構造方法只會調用一次
Singleton(){
singleton = new SingletonSpecial();
}
public SingletonSpecial getInstance(){
return singleton;
}
}
}
7 【使用靜態內部類】
這種方式在 Singleton 類被裝載時並不會立即實例化,而是在需要實例化時,調用getInstance方法,才會加載 SingletonInstance 類,從而完成 Singleton 的實例化。
使用 static final 修飾之后 JVM 就會保證 instance 只會初始化一次且不會改變。
@ThreadSafe
@Recommend
public class SingletonExample7 {
private SingletonExample7(){}
private static class SingletonInstance{
private static final SingletonExample7 instance = new SingletonExample7();
}
public static SingletonExample7 getInstance(){
return SingletonInstance.instance;
}
}
總結一下,今天主要說了單例模式的實現,並且在這中間,復習了一下前面說的線程安全的應用。若是對線程安全的原理以及實現有不懂的可以回頭看看前面幾篇文章。
