單例模式實現方式
轉自https://www.cnblogs.com/rjzheng/p/8946889.html
先看一副漫畫:
OK,回顧一下小灰的遭遇,上述漫畫中所提出的那些問題主要有以下三點:
1.為什麼靜態內部類的單例模式是最推薦的?
2.如何在反射的情況下保證單例?
3.如何在反序列化中保證單例?
針對上述三個問題有了這篇文章,以一種循序漸進的方式引出最後一種單例設計模式,希望對大家能夠有所幫助。
1.餓漢式
直接上程式碼:
package singleton; public class Singleton1 { private static Singleton1 instance = new Singleton1(); private Singleton1 (){} public static Singleton1 getInstance() { return instance; } }
優點就是執行緒安全,缺點很明顯就是浪費記憶體,因為它在類載入的時候就做例項化物件的操作了。於是就有了懶漢式的單例模式
2.懶漢式
2.1懶漢式V1
直接上程式碼:
public class LazySingleton1 { private static LazySingleton1 instance; private LazySingleton1 (){} public static LazySingleton1 getInstance() { if (instance == null) { instance = new LazySingleton1(); } return instance; } }
然而這一版不是執行緒安全的,於是乎為了執行緒安全,就在getInstance()方法上加synchronized修飾符,於是getInstance()方法如下:
public static synchronized LazySingleton1 getInstance() {
if (instance == null) {
instance = new LazySingleton1();
}
return instance;
}
然而,將synchronized加在方法上對效能是大打折扣(synchronized會造成執行緒阻塞),於是乎又提出一種雙重校驗鎖的單例設計模式,既保證了執行緒安全又提高了效能。如下:
public static LazySingleton1 getInstance() {
if (instance == null) {
synchronized (LazySingleton1.class) {
if (instance == null) {
instance = new LazySingleton1();
}
}
}
return instance;
}
2.2懶漢式V2
懶漢式V1的最後一個雙重校驗鎖版,不能效能再如何優化,還是使用了synchronized修飾符,既然使用了該修飾符,那麼對效能多多少少都會造成一些影響,於是乎懶漢式V2版誕生。不過在講該版之前,先來複習下內部類的載入機制,程式碼如下:
public class OuterTest {
static {
System.out.println("load outer class...");
}
// 靜態內部類
static class StaticInnerTest {
static {
System.out.println("load static inner class...");
}
static void staticInnerMethod() {
System.out.println("static inner method...");
}
}
public static void main(String[] args) {
OuterTest outerTest = new OuterTest(); // 此刻其內部類是否也會被載入?
System.out.println("===========分割線===========");
OuterTest.StaticInnerTest.staticInnerMethod(); // 呼叫內部類的靜態方法
}
}
輸出結果:
load outer class...
===========分割線===========
load static inner class...
static inner method
因此有如下結論:
1.載入一個類時,其內部類不會同時被載入
2.當且僅當其某個靜態成員(靜態域、構造器、靜態方法等)被呼叫時,類才被載入
基於上面的結論我們有了懶漢式V2版,程式碼如下:
public class LazySingleton2 {
private LazySingleton2() {
}
static class SingletonHolder {
private static final LazySingleton2 instance = new LazySingleton2();
}
public static LazySingleton2 getInstance() {
return SingletonHolder.instance;
}
}
由於物件例項化是在內部類載入的時候構建的,因此該版是執行緒安全的(因為在方法中建立物件才存在併發問題,靜態內部類隨著方法呼叫而被載入,且只加載一次,不存在併發問題,所以是執行緒安全的)。另外,在getInstance()方法中沒有使用synchronized關鍵字,因此沒有造成多餘的效能損耗。當LazySingleton2類載入時,其靜態內部類SingletonHolder並沒有被載入,因此instance物件並沒有初始化。而我們在呼叫LazySingleton2.getInstance()方法時,內部類SingletonHolder被載入,此時單例物件才被構建。因此,這種寫法節約空間,達到懶載入的目的,該版也是眾多部落格中推薦的版本。
ps:其實列舉單例模式也有類似的效能,但是因為可讀性的原因,並不是最推薦的版本。
2.3懶漢式V3
然而,懶漢式V2版在反射的作用下,單例結構是會被破壞的,測試程式碼如下:
public class LazySingleton2Test {
public static void main(String[] args) {
//建立第一個例項
LazySingleton2 instance1 = LazySingleton2.getInstance();
//通過反射建立第二個例項
LazySingleton2 instance2 = null;
try {
Class<LazySingleton2> clazz = LazySingleton2.class;
Constructor<LazySingleton2> cons = clazz.getDeclaredConstructor();
cons.setAccessible(true);
instance2 = cons.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
//檢查兩個例項的hash值
System.out.println("Instance 1 hash:" + instance1.hashCode());
System.out.println("Instance 2 hash:" + instance2.hashCode());
}
}
輸出結果:
Instance 1 hash:1694819250
Instance 2 hash:1365202186
根據雜湊值可以看出,反射破壞了單例的特性,因此懶漢式V3版誕生了:
public class LazySingleton3 {
private static boolean initialized = false;
private LazySingleton3() {
synchronized (LazySingleton3.class) {
if (initialized == false) {
initialized = !initialized;
} else {
throw new RuntimeException("單例已被破壞");
}
}
}
static class SingletonHolder {
private static final LazySingleton3 instance = new LazySingleton3();
}
public static LazySingleton3 getInstance() {
return SingletonHolder.instance;
}
}
此時再執行一次測試類時,出現如下提示:
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at test.LazySingleton3Test.main(LazySingleton3Test.java:21)
Caused by: java.lang.RuntimeException: 單例已被破壞
at singleton.LazySingleton3.<init>(LazySingleton3.java:12)
... 5 more
這裡就保證了反射無法破壞其單例特性。
2.4懶漢式V4
在分散式系統中,有些情況下需要在單例類中實現Serializable介面。這樣可以在檔案系統中儲存它的狀態並且在稍後的某一時間點取出。
讓我們測試這個懶漢式V3版在序列化和反序列化之後是否仍然保持單例:
先將
public class LazySingleton3
修改為:
public class LazySingleton3 implements Serializable
測試類如下:
public class LazySingleton3Test {
public static void main(String[] args) {
try {
LazySingleton3 instance1 = LazySingleton3.getInstance();
ObjectOutput out = null;
out = new ObjectOutputStream(new FileOutputStream("filename.ser"));
out.writeObject(instance1);
out.close();
//deserialize from file to object
ObjectInput in = new ObjectInputStream(new FileInputStream("filename.ser"));
LazySingleton3 instance2 = (LazySingleton3) in.readObject();
in.close();
System.out.println("instance1 hashCode=" + instance1.hashCode());
System.out.println("instance2 hashCode=" + instance2.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
}
}
輸出結果:
instance1 hashCode=2051450519
instance2 hashCode=1510067370
顯然我們又看到了兩個例項類。為了避免這個問題我們需要提供readResolve()方法的實現。readResolve()方法代替了從流中讀取物件。這就確保了在序列化和反序列化的過程中沒人可以建立新的例項。
因此,我們提供懶漢式V4版程式碼如下:
public class LazySingleton4 implements Serializable {
private static boolean initialized = false;
private LazySingleton4() {
synchronized (LazySingleton4.class) {
if (initialized == false) {
initialized = !initialized;
} else {
throw new RuntimeException("單例已被破壞");
}
}
}
static class SingletonHolder {
private static final LazySingleton4 instance = new LazySingleton4();
}
public static LazySingleton4 getInstance() {
return SingletonHolder.instance;
}
private Object readResolve() {
return getInstance();
}
}
此時,再執行測試類,其輸出結果如下:
instance1 hashCode=2051450519
instance2 hashCode=2051450519
總結
本文給出了多個版本的單例模式供我們在專案中使用。實際上在實際專案中一般從懶漢式V2、懶漢式V3、懶漢式V4中,根據實際情況三選一即可,並不是非要選擇懶漢式V4作為單例來實現。如果沒有特殊需求,懶漢式V2足夠用了。