單例模式可能是後端學習者接觸到的第一種設計模式,可是單例模式真的有那麼簡單嗎?在併發模式下會出現什麼樣的問題?在學習了前面的併發知識後,我們來看看究極版的單例模式應該怎麼寫。
一、單例模式第一版
我們最初接觸到的單例模式一般就是懶漢模式與餓漢模式。我們先來看看怎麼寫:
//懶漢模式
public class Singleton {
private Singleton() {} //私有構造函數
private static Singleton instance = null; //單例對象
//靜態工廠方法
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
//餓漢模式
public class Singleton {
private Singleton() {} //私有構造函數
private static Singleton instance = new Singleton(); //單例對象
//靜態工廠方法
public static Singleton getInstance() {
return instance;
}
}
-
要想讓一個類只能構建一個對象,自然不能讓它隨便去做new操作,因此Signleton的構造方法是私有的。
-
instance是Singleton類的靜態成員,也是我們的單例對象。它的初始值可以寫成Null,也可以寫成new Singleton()。至於其中的區別後來會做解釋。
-
getInstance是獲取單例對象的方法。
這兩個名字很形象:餓漢主動找食物吃,懶漢躺在地上等着人喂。
1、餓漢式:在程序啓動或單件模式類被加載的時候,單件模式實例就已經被創建。
2、懶漢式:當程序第一次訪問單件模式實例時才進行創建。
懶漢模式加載快執行慢,但是有線程安全問題,容易引起不同步問題,所以應該創建同步"鎖"。
二、單例模式第二版
懶漢模式的線程安全問題主要在if (instance == null)
這句判斷是否爲空上。在多線程的環境下,可能有多個線程同時通過這個判斷。這樣一來,就有可能同時創建多個實例。讓我們來對代碼做一下修改:
public class Singleton {
private Singleton() {} //私有構造函數
private static Singleton instance = null; //單例對象
//靜態工廠方法
public static Singleton getInstance() {
if (instance == null) { //雙重檢測機制
synchronized (Singleton.class){ //同步鎖
if (instance == null) { //雙重檢測機制
instance = new Singleton();
}
}
}
return instance;
}
}
-
爲了防止new Singleton被執行多次,因此在new操作之前加上Synchronized 同步鎖,鎖住整個類(注意,這裏不能使用對象鎖)。
-
進入Synchronized 臨界區以後,還要再做一次判空。因爲當兩個線程同時訪問的時候,線程A構建完對象,線程B也已經通過了最初的判空驗證,不做第二次判空的話,線程B還是會再次構建instance對象。
然而,這種方法也有一定的缺席。
三、單例模式第三版
假設這樣的場景,當兩個線程一先一後訪問getInstance方法的時候,當A線程正在構建對象,B線程剛剛進入方法。
這種情況表面看似沒什麼問題,要麼Instance還沒被線程A構建,線程B執行 if(instance == null)的時候得到true;要麼Instance已經被線程A構建完成,線程B執行 if(instance == null)的時候得到false。
我們之前在JAVA併發編程(一):理解volatile關鍵字學習過指令重排的知識,instance = new Singleton()
這個操作不是一個原子操作,它在執行的時候要經歷以下三個步驟:
memory =allocate(); //1:分配對象的內存空間
ctorInstance(memory); //2:初始化對象
instance =memory; //3:設置instance指向剛分配的內存地址
所以這裏有可能出現如下情況:
當線程A執行完1,3,時,instance對象還未完成初始化,但已經不再指向null。此時如果線程B搶佔到CPU資源,執行 if(instance == null)的結果會是false,從而返回一個沒有初始化完成的instance對象。
如何避免這一情況呢?我們需要在instance對象前面增加一個修飾符volatile。
public class Singleton {
private Singleton() {} //私有構造函數
private volatile static Singleton instance = null; //單例對象
//靜態工廠方法
public static Singleton getInstance() {
if (instance == null) { //雙重檢測機制
synchronized (Singleton.class){ //同步鎖
if (instance == null) { //雙重檢測機制
instance = new Singleton();
}
}
}
return instance;
}
}
三、其他方式實現單例模式
實現單例模式的手段還有很多,我們再來看一些別的實現方式。
①靜態內部類實現單例模式
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
需要注意的是:
-
從外部無法訪問靜態內部類LazyHolder,只有當調用Singleton.getInstance方法的時候,才能得到單例對象INSTANCE。
-
INSTANCE對象初始化的時機並不是在單例類Singleton被加載的時候,而是在調用getInstance方法,使得靜態內部類LazyHolder被加載的時候。因此這種實現方式是利用classloader的加載機制來實現懶加載,並保證構建單例的線程安全。
-
靜態內部類與餓漢&懶漢模式存在共同的問題:無法防止利用反射來重複構建對象。
②枚舉實現單例模式
可以防止反射的無懈可擊的單例模式代碼:
public class SingletonExample {
// 私有構造函數
private SingletonExample() {
}
public static SingletonExample getInstance() {
return Singleton.INSTANCE.getInstance();
}
private enum Singleton {
INSTANCE;
private SingletonExample singleton;
// JVM保證這個方法絕對只調用一次
Singleton() {
singleton = new SingletonExample();
}
public SingletonExample getInstance() {
return singleton;
}
}
}
使用枚舉實現的單例模式不僅能夠防止反射構造對象,而且可以保證線程安全。不過這種方式也有一個缺點,那就是不能實現懶加載,它的單例模式是在枚舉類被加載的時候進行初始化的。
參考文章
本文作者: catalinaLi
本文鏈接: http://catalinali.top/2018/singletonPattern/
版權聲明: 原創文章,有問題請評論中留言。非商業轉載請註明作者及出處。