用私有構造器或枚舉類型強化Singleton屬性
單例模式(Singleton Pattern)無疑是筆試面試中被問得最多的問題之一。單例模式雖然看似簡單,但是仍有很多東西值得思考。
GOF是這麼定義單例模式的:
確保一個類只有一個實例,並提供一個全局訪問點。
通常實現單例都需要我們私有化構造器,讓對象無法在外部創建,同時提供一個外部訪問的方法返回這個單例對象。
通常單例分爲兩大類實現:餓漢式和懶漢式。
餓漢式單例
所謂“餓漢式單例”就是在類加載器加載這個類的時候就立馬創建這個類的單例對象。
1. 使用靜態常量域提供外部訪問
public class Singleton {
public static final Singleton INSTANCE = new Singleton();
private Singleton() {/* 私有化構造器 */}
public void doSomething() {
...
}
}
2. 使用靜態工廠方法提供外部訪問
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {/* 私有化構造器 */}
public static Singleton getInstance() {
return INSTANCE;
}
public void doSomething() {
...
}
}
靜態工廠方法相對於靜態常量域的好處是可以在不改變API的前提下,可以改變該類是否爲單例的想法。
3. 防止反射調用私有構造器
上面的私有構造方法仍有缺少保護,外部的調用者仍可以使用反射機制AccessibleObject.setAccessible()
方法來訪問私有構造方法:
public class SingletonTest {
@Test
public void testReflect()
throws NoSuchMethodException, SecurityException, InstantiationException,
IllegalAccessException, IllegalArgumentException, InvocationTargetException {
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton newInstance = constructor.newInstance();
Assert.assertNotEquals(Singleton.getInstance(), newInstance);
}
}
所有我們要對構造方法更狠一點:
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
if(INSTANCE != null)
throw new IllegalStateException("The object can only be created once");
}
public static Singleton getInstance() {
return INSTANCE;
}
public void doSomething() {
...
}
}
4. 防止反序列化導致的多個實例
如果我們的Singleton類實現了Serializable接口,上面構造器檢測拋異常的方式也無法阻止反序列化創建新實例。
public class SingletonTest {
@Test
public void testSeriliable() {
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("D:/singleton.obj"))) {
oos.writeObject(Singleton.getInstance());
} catch (Exception ignore) {}
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("D:/singleton.obj"))) {
Object newInstance = ois.readObject();
Assert.assertNotEquals(Singleton.getInstance(), newInstance);
} catch (Exception ignore) {}
}
}
我們只需要定義一個readResolve
即可:
public class Singleton implements Serializable {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
if (INSTANCE != null)
throw new RuntimeException("The object can only be created once");
}
public static Singleton getInstance() {
return INSTANCE;
}
public void doSomething() {
System.out.println("do something");
}
// 訪問修飾符可以任意
private Object readResolve() {
return INSTANCE;
}
}
這個方式是《Effective Java》中推薦的做法。關於readResolve的原理,可以參考Java對象序列化規範或者StackOverflow對這個問題的討論:用Java如何高效的實現單例
5. 使用單元素枚舉類實現單例
使用枚舉類實現單例是《Effective Java》中推薦的最佳方法:
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("do something");
}
}
這種方式和最開始的使用靜態常量域的方式差不多,但是它更簡潔;由於枚舉類的特性,它能絕對地防止多次實例化,並且無償地提供了序列化的機制,這種方式完全避免了前面的反射和反序列化的問題。
懶漢式單例
所謂“懶漢式單例”就是在加載這個類的時候不立即創建對象,而是等到第一次用到單例對象的時候臨時創建單例對象。對於一些大對象來說,懶加載還是很有必要的。
1. 使用靜態工廠方法實現懶漢式單例
很顯然爲了能實現懶漢式單例,我們肯定不能直接使用靜態常量了,所以只能用靜態工廠方法實現懶漢式單例了。
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {/* 私有化構造器 */}
public static Singleton getInstance() {
if(INSTANCE == null) {
INSTANCE = new Singleton()
}
return INSTANCE;
}
public void doSomething() {
...
}
}
2. 同步方法解決多線程問題
上面的單例在單線程環境下確實沒啥毛病,但是在多線程環境下可能就會出現問題:可能會有多個進程同時通過 (INSTANCE == null)
的條件檢查,於是,多個實例就創建出來,如果在C++裏面創建的對象沒有銷燬就會導致內存泄漏(多線程的世界真可怕(╯︵╰)),不過好在java天生支持多線程同步,我們可以在靜態工廠方法上添加synchronized
關鍵字實現線程同步訪問:
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
public void doSomething() {
...
}
}
3. 使用同步代碼塊減小鎖粒度
線程同步問題是解決了,但是每次調用getInstance方法的時候都去檢查同步鎖肯定會影響程序執行的效率,雖然現在JVM對synchronized
的優化做的越來越好,但是調用次數多了整體效率肯定下降,所以我們有必要減小鎖粒度。
第一步:
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
上面的做法可行嗎,很顯然是不可以滴,多個線程仍然會進入 (INSTANCE == null)
條件,這裏的同步只是讓多個線程排隊去創建對象而已。
第二步:
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
這種做法,和使用靜態代碼塊差不多,每次調用getInstance方法的時候仍然會去檢查同步鎖。
第三步:
public static Singleton getInstance() {
// DCL
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
這個Double Checked Locking總該差不多了吧,不好意思,還不夠。
在多處理器共享內存(shared memory multiprocessors)或者編譯器優化(optimizing compilers)進行指令重排的情況下仍有可能會導致創建多個對象。
對於這個問題我這有兩種解釋:
1. 多處理器共享內存:
處理器p1創建完Singleton對象,並把它賦值給INSTANCE變量走出了同步代碼塊,但是INSTANCE變量並沒有立即反映到內存上(處理器直接操作cache高速緩存,並不直接操作內存),這時處理器p2進入同步代碼塊後發現INSTANCE仍爲null,就會創建另一個Singleton對象。
2. 編譯器優化時進行指令重排:
INSTANCE = new Singleton();
這句話大概會分三步走:
new
:要求操作系統進行內存分配Singleton()
:調用類的構造函數對分配的內存進行初始化=
:將新創建的對象的地址賦值給INSTANCE變量
但是JVM在將字節碼翻譯成機器碼的過程中可能會對指令進行重新排列(學過編譯原理的應該都知道編譯器會對指令進行優化重排),這個時候第2步和第3步的先後順序就不確定了,如果線程1按照1->3->2
的順序執行,線程2得到的就是一個還未初始化的實例對象,然後就報錯了。
這個時候我們用上volatile
關鍵字就能解決了。
關於volatile關鍵字的一些用法我之前也寫過一篇文章:http://blog.csdn.net/Holmofy/article/details/73824757
第四步:
public class Singleton {
// 使用volatile關鍵字
private static volatile Singleton INSTANCE;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
public void doSomething() {
...
}
}
4. 使用私有靜態內部類保存單例
上面的方法也太麻煩了吧,一個單例都要搞老半天,有沒有更簡單的方法。
《Effective Java》第一版推薦的方法:
public class Singleton {
// 靜態內部類包裝實例
private static class InstanceHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return InstanceHolder.INSTANCE;
}
public void doSomething() {
System.out.println("do something");
}
}
因爲使用了私有靜態內部類,所以只有當第一次調用getInstance方法時纔會加載這個靜態內部類,然後纔會去創建對象;讀取的時候又沒有進行線程同步不影響性能(簡直完美了)。
什麼時候單例不是單例
之前在StackOverflow中看到有討論不同類加載器下單例模式會出現問題,然後在Oracle官網找到了這篇文章
1. 兩個或多個JVM中有多個單例對象
由於程序在不同的JVM上運行,很明顯每個JVM都會有自己的Singleton實例。但是在基於分佈式技術的系統(如EJB,RMI和Jini)可以讓不同的JVM中的兩個對象保持相同的狀態。
2. 不同的類加載器會創建多個單例對象
一個JVM可以有多個ClassLoader,當兩個ClassLoader加載一個類時,實際上有兩個class副本,然後每個class都有它自己的Singleton實例。有一些Servlet容器(比如iPlanet)每個Servlet都有自己的類加載器,那麼兩個不同的Servlet將訪問不同的Singleton對象。如果你的程序中也有自定義ClassLoader,那麼務必注意這個問題。
參考鏈接:
StackOverflow: https://stackoverflow.com/questions/70689/what-is-an-efficient-way-to-implement-a-singleton-pattern-in-java/71399#71399
The “Double-Checked Locking is Broken” Declaration:http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
How to Simply Singleton:https://www.javaworld.com/article/2073352/core-java/simply-singleton.html
When is a Singleton not a Singleton? http://www.oracle.com/technetwork/articles/java/singleton-1577166.html