本文將探討單例模式的各種情況,并給出相應(yīng)的建議。單例模式應(yīng)該是設(shè)計模式中比較簡單的一個,但是在多線程并發(fā)的環(huán)境下使用卻是不那么簡單了。
首先看最原始的單例模式。
1 package xylz.study.singleton;
2
3 public class Singleton {
4
5 private static Singleton instance = null;
6
7 private Singleton() {
8 }
9
10 public static Singleton getInstance() {
11 if (instance == null) {
12 instance = new Singleton();
13 }
14 return instance;
15 }
16 }
17
顯然這個寫法在單線程環(huán)境下非常好,但是多線程會導(dǎo)致多個實例出現(xiàn),這個大家都能理解。
最簡單的改造方式是添加一個同步鎖。
1 package xylz.study.singleton;
2
3 public class SynchronizedSingleton {
4
5 private static SynchronizedSingleton instance = null;
6
7 private SynchronizedSingleton() {
8 }
9
10 public static synchronized SynchronizedSingleton getInstance() {
11 if (instance == null) {
12 instance = new SynchronizedSingleton();
13 }
14 return instance;
15 }
16 }
17
顯然上面的方法避免了并發(fā)的問題,但是由于我們只是在第一次構(gòu)造對象的時候才需要同步,以后就不再需要同步,所以這里不可避免的有性能開銷。于是將鎖去掉采用靜態(tài)的屬性來解決同步鎖的問題。
1 package xylz.study.singleton;
2
3 public class StaticSingleton {
4
5 private static StaticSingleton instance = new StaticSingleton();
6
7 private StaticSingleton() {
8 }
9
10 public static StaticSingleton getInstance() {
11 return instance;
12 }
13 }
14
上面的方法既沒有鎖又解決了性能問題,看起來已經(jīng)滿足需求了。但是追求“完美”的程序員想延時加載對象,希望在第一次獲取的時候才構(gòu)造對象,于是大家非常聰明的進行改造,也即非常出名的雙重檢查鎖機制出來了。
1 package xylz.study.singleton;
2
3 public class DoubleLockSingleton {
4
5 private static DoubleLockSingleton instance = null;
6
7 private DoubleLockSingleton() {
8 }
9
10 public static DoubleLockSingleton getInstance() {
11 if (instance == null) {
12 synchronized (DoubleLockSingleton.class) {
13 if (instance == null) {
14 instance = new DoubleLockSingleton();
15 }
16 }
17 }
18 return instance;
19 }
20 }
21
雙重鎖機制看起來非常巧妙的避免了上面的問題。但是真的是這樣的嗎?文章《雙重檢查鎖定及單例模式》中談到了非常多演變的雙重鎖機制帶來的問題,包括比較難以理解的指令重排序機制等。總之就是雙重檢查鎖機制仍然對導(dǎo)致錯誤問題而不是性能問題。
一種避免上述問題的解決方案是使用volatile關(guān)鍵字,此關(guān)鍵字保證對一個對象修改后能夠立即被其它線程看到,也就是避免了指令重排序和可見性問題。參考文章
所以上面的寫法就變成了下面的例子。
package xylz.study.singleton;
public class DoubleLockSingleton {
private static volatile DoubleLockSingleton instance = null;
private DoubleLockSingleton() {
}
public static DoubleLockSingleton getInstance() {
if (instance == null) {
synchronized (DoubleLockSingleton.class) {
if (instance == null) {
instance = new DoubleLockSingleton();
}
}
}
return instance;
}
}
于是繼續(xù)改造,某個牛人利用JVM的特性來解決上述問題,具體哪個牛人我忘記了,但是不是下面文章的作者。
(1)《Java theory and practice: Fixing the Java Memory Model, Part 2》
(2)《Initialize-On-Demand Holder Class and Singletons》
1 package xylz.study.singleton;
2
3 public class HolderSingleton {
4
5 private static class HolderSingletonHolder {
6
7 static HolderSingleton instance = new HolderSingleton();
8 }
9
10 private HolderSingleton() {
11 //maybe throw an Exception when doing something
12 }
13
14 public static HolderSingleton getInstance() {
15 return HolderSingletonHolder.instance;
16 }
17 }
18
上述代碼看起來解決了上面單例模式遇到的所有問題,而且實際上工作的很好,沒有什么問題。但是卻有一個致命的問題,如果第11行拋出了一個異常,也就是第一次構(gòu)造函數(shù)失敗將導(dǎo)致永遠無法再次得到構(gòu)建對象的機會。
使用下面的代碼測試下。
1 package xylz.study.singleton;
2
3 public class HolderSingletonTest {
4
5 private static class HolderSingletonHolder {
6
7 static HolderSingletonTest instance = new HolderSingletonTest();
8 }
9
10 private static boolean init = false;
11
12 private HolderSingletonTest() {
13 //maybe throw an Exception when doing something
14 if(!init) {
15 init=true;
16 throw new RuntimeException("fail");
17 }
18 }
19
20 public static HolderSingletonTest getInstance() {
21 return HolderSingletonHolder.instance;
22 }
23 public static void main(String[] args) {
24 for(int i=0;i<3;i++) {
25 try {
26 System.out.println(HolderSingletonTest.getInstance());
27 } catch (Exception e) {
28 System.err.println("one->"+i);
29 e.printStackTrace();
30 }catch(ExceptionInInitializerError err) {
31 System.err.println("two->"+i);
32 err.printStackTrace();
33 }catch(Throwable t) {
34 System.err.println("three->"+i);
35 t.printStackTrace();
36 }
37 }
38 }
39 }
40
很不幸將得到以下輸出:
1 two->0
2 java.lang.ExceptionInInitializerError
3 at xylz.study.singleton.HolderSingletonTest.getInstance(HolderSingletonTest.java:21)
4 at xylz.study.singleton.HolderSingletonTest.main(HolderSingletonTest.java:26)
5 Caused by: java.lang.RuntimeException: fail
6 at xylz.study.singleton.HolderSingletonTest.<init>(HolderSingletonTest.java:16)
7 at xylz.study.singleton.HolderSingletonTest.<init>(HolderSingletonTest.java:12)
8 at xylz.study.singleton.HolderSingletonTest$HolderSingletonHolder.<clinit>(HolderSingletonTest.java:7)
9
2 more
10 three->1
11 java.lang.NoClassDefFoundError: Could not initialize class xylz.study.singleton.HolderSingletonTest$HolderSingletonHolder
12 at xylz.study.singleton.HolderSingletonTest.getInstance(HolderSingletonTest.java:21)
13 at xylz.study.singleton.HolderSingletonTest.main(HolderSingletonTest.java:26)
14 three->2
15 java.lang.NoClassDefFoundError: Could not initialize class xylz.study.singleton.HolderSingletonTest$HolderSingletonHolder
16 at xylz.study.singleton.HolderSingletonTest.getInstance(HolderSingletonTest.java:21)
17 at xylz.study.singleton.HolderSingletonTest.main(HolderSingletonTest.java:26)
18 很顯然我們想著第一次加載失敗第二次能夠加載成功,非常不幸,JVM一旦加載某個類失敗將認為此類的定義有問題,將來不再加載,這樣就導(dǎo)致我們沒有機會再加載。目前看起來沒有辦法避免此問題。如果要使用JVM的類加載特性就必須保證類加載一定正確,否則此問題將比并發(fā)和性能更嚴重。如果我們的類需要初始話那么就需要想其它辦法避免在構(gòu)造函數(shù)中完成。看起來像是又回到了老地方,難道不是么?
總之,結(jié)論是目前沒有一個十全十美的單例模式,而大多數(shù)情況下我們只需要滿足我們的需求就行,沒必有特意追求最“完美”解決方案。
原文[
http://www.imxylz.info/p/177.html]
©2009-2014 IMXYLZ
|求賢若渴