雙重鎖檢查單例、列舉單例、靜態內部類單例
阿新 • • 發佈:2021-01-18
單例最基本要素:
- 私有靜態屬性,用於存取類的唯一例項。
- 公共靜態方法,用於提供對該唯一例項的存取訪問,如果例項未建立,則建立該例項。
- 用於限制類再次例項化的方式。通常使用私有構建函式的方式來實現。
最簡單的單例
public class Singleton {
private Singleton() {} //私有建構函式
private static Singleton instance = null; //單例物件
//靜態工廠方法
public static Singleton getInstance() {
if ( instance == null) {
instance = new Singleton();
}
return instance;
}
}
上述單例執行緒不安全,如在instance未例項化時,就有多個執行緒一起呼叫getInstance()方法,則會創建出多個物件。
雙重加鎖單例實現
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;
}
}
經過兩個加鎖檢測,看上去是很安全了,但是還是有執行緒安全問題,因為涉及到了JVM編譯器的指令重排。
指令重排是什麼意思呢?
比如java中簡單的一句 instance = new Singleton,會被編譯器編譯成如下JVM指令:
memory =allocate(); //1:分配物件的記憶體空間
ctorInstance(memory); //2:初始化物件
instance =memory; //3:設定instance指向剛分配的記憶體地址
但是這些指令順序並非一成不變,有可能會經過JVM和CPU的優化,指令重排成下面的順序:
memory =allocate(); //1:分配物件的記憶體空間
instance =memory; //3:設定instance指向剛分配的記憶體地址
ctorInstance(memory); //2:初始化物件
我們需要在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;
}
}
volatile關鍵字:
- 阻止了變數訪問前後的指令重排,保證了指令執行順序。
- 保證執行緒訪問的變數值是主記憶體中的最新值。
用靜態內部類實現單例模式:
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的載入機制來實現懶載入,並保證構建單例的執行緒安全。
雙重鎖檢查單例、靜態內部類單例都可以實現執行緒安全的單例模式,但是不能防止用反射的方式打破單例,如下。
//獲得構造器
Constructor con = Singleton.class.getDeclaredConstructor();
//設定為可訪問
con.setAccessible(true);
//構造兩個不同的物件
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//驗證是否是不同物件
System.out.println(singleton1.equals(singleton2));
如何創建出執行緒安全又不能被反射破壞單例性的單例呢?
答案是用列舉實現單例模式。
列舉實現單例模式
/**
* 最完美的單例模式
* @author Administrator
*
*/
public class MySingleton {
public enum MyEnumSingle{
INSTANCE;
private MySingleton singleOne;
private MyEnumSingle(){
System.out.println("初始化單例");
singleOne = new MySingleton();
}
public MySingleton getInstance(){
return singleOne;
}
}
private MySingleton(){}
public static MySingleton getInstance(){
return MyEnumSingle.INSTANCE.getInstance();
}
}
- 列舉模式的單例是餓漢模式
- 使用列舉實現的單例模式,不但可以防止利用反射強行構建單例物件,而且可以在列舉類物件被反序列化的時候,保證反序列的返回結果是同一物件。