單例模式,多是惟一一個咱們談到時,每一個工程師都會二眼放光,口若懸河的模式,除了它最簡單直接外,還由於咱們「自覺得」對它瞭如指掌,這篇文章帶你們作個總結,死磕單例模式的方方面面。java
大概有如下二種場景須要單例安全
實現一個單例,咱們要考慮如下幾點。bash
總結一句話,如何線程安全的建立惟一實例對象。 先看一下Java中如何具體實現單例。app
public class UserManager {
private static UserManager instance = new UserManager();
private UserManager() { }
public static UserManager getInstance() {
return instance;
}
}
複製代碼
首先經過私有化構造器,禁止了外部new的可能性,而後instance是static修飾的,因此在類被首次加載後,調用init 的時候,instance會被初始化,JVM保證類加載過程的線程安全,因此instance也是線程安全的。 由於在類加載初始化的時候,單例就被建立出來了,因此相對於按需延時加載,這種寫法若是有大量單例須要建立,在系統剛啓動時內存壓力比較大。同時上面的寫法也沒有可以禁止序列化和反射對單例的破壞(關於這個咱們放到最後來解決)。ide
private static volatile UserManager instance;
private UserManager() {}
public static UserManager getInstance() {
if (instance == null) {
synchronized (UserManager.class) {
if (instance == null) {
instance = new UserManager();
}
}
}
return instance;
}
複製代碼
這也是很經典的單例實現,經過二次判空檢查,並且只有在第一次初始化時getInstance會加鎖,後面的獲取都不會加鎖,時間和空間效率都很高。 這裏要注意的一點是instance必定要加volatile修飾符。關於這一點,不少同窗可能會理解的不夠全面,下面我來詳細分析一下。 首先由於在建立UserManager的時候,咱們是有加鎖的,並且鎖的對象是UserManager這個Class對象。好比線程A得到了鎖,開始new UserManager(), 而且賦值給了instance,這時候線程B開始調用getInstance()來獲取單例對象,因爲鎖擁有可見性,因此線程A的賦值happen-before線程B的獲取,表面上看一切很完美,可是在jdk1.5以前,volatile語意尚未被增強,不能禁止指令重排序。ui
instance = new UserManager();
複製代碼
這條語句,其實能夠被看作三條僞代碼。spa
private UserManager() {}
private static UserManager getInstance() {
return SingltonHolder.sInstance;
}
private static class SingltonHolder {
private static UserManager sInstance = new UserManager();
}
複製代碼
靜態內部類的方式實現的單例一樣是線程安全的,由JDK來保證。同時也具備延時加載的特性。這種寫法對比Double-Check更簡潔,推薦使用。線程
Effective Java中推薦使用枚舉的方式來實現單例,咱們來看一下code
public enum UserManager {
INSTANCE;
}
複製代碼
很簡潔,但咱們知道,枚舉是Java提供的語法糖,咱們解語法糖看下它的具體代碼對象
public final class com.dig.deep.design.singlton.UserManager extends java.lang.Enum<com.dig.deep.design.singlton.UserManager> {
public static final com.dig.deep.design.singlton.UserManager INSTANCE;
private static final com.dig.deep.design.singlton.UserManager[] $VALUES;
public static com.dig.deep.design.singlton.UserManager[] values();
public static com.dig.deep.design.singlton.UserManager valueOf(java.lang.String);
private com.dig.deep.design.singlton.UserManager();
static {};
}
複製代碼
能夠看到解語法糖後的UserManager,構造器也是私有的,有個一個static final 的INSTANCE類常量,能夠大膽猜想,JVM在加載枚舉類時,會給全部的枚舉項賦值,同時會保證過程的線程安全。
咱們上面有提到過,一個完整的單例須要作到防止
try {
UserManager userManager = UserManager.instance;
FileOutputStream fileOutputStream = new FileOutputStream("user");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(userManager);
FileInputStream fileInputStream = new FileInputStream("user");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
UserManager newUserManager = (UserManager) objectInputStream.readObject();
System.out.println("is equal: " + (userManager == newUserManager));
} catch (IOException e) {
e.printStackTrace();
}
複製代碼
輸出是false,通過序列化和反序列化後,生成了二個單例對象,顯然破壞了單例的語意,解決這個問題,咱們能夠給UserManager增長一個readResolve方法, 並在其中返回單例對象。
private Object readResolve() {
return instance;
}
複製代碼
Class clazz = UserManager.class;
Constructor[] constructors = clazz.getDeclaredConstructors();
try {
constructors[0].setAccessible(true);
UserManager newUserManager = (UserManager) constructors[0].newInstance();
} catch (Exception e) {
e.printStackTrace();
}
複製代碼
若是開發者真的使用反射來做惡,誰能攔得住呢?雖然反射最終調用的仍是咱們的私有構造器,在構造器裏面咱們能夠加一些判斷邏輯,可是仍是不能涵蓋全部的狀況,由於畢竟咱們的單例實現多種多樣,有延時加載的,有非延時加載的。 可是經過Enum方式實現的單例是不可以被反射的,若是嘗試反射Enum的構造器,會拋出一個異常,因此Enum方式實現的單例對反射安全。
儘可能不要給單例實現cloneable接口,若是非要實現,也在重寫的clone方法裏,返回此單例對象。
@Override
protected Object clone() throws CloneNotSupportedException {
return getInstance();
}
複製代碼
單例模式比較簡單,同時咱們平常工做也用的很頻繁,工程師有必要對它有個全面瞭解,在選擇實現方案時作到心中有數。