今天和同事聊起了單例模式的線程安全,我說如果不做任何措施,單例模式在多線程下是不安全的,得到的“單例”實際上並不是單例。但是為什么不是單例呢?由此我上網查了一下,在使用單例模式時,一定要注意線程安全問題,之前的寫法沒有任何問題。如下:
1 package day_5_singleton; 2 3 /** 4 * 單例 5 * 6 * @author turbo 7 * 8 * 2016年9月8日 9 */ 10 public class Singleton { 11 private static Singleton instance; 12 13 private Singleton() { 14 } 15 16 public static synchronized Singleton GetInstance() { 17 18 if (instance == null) { 19 instance = new Singleton(); 20 } 21 22 return instance; 23 } 24 }
問題就在於,synchronized對整個方法加鎖,形成同步機制,這樣雖然解決了單例模式的線程安全問題,但是卻產生另外一個問題性能問題,對方法加鎖這個顆粒度有點大,我們稍微改進一下。如下:
1 package day_5_singleton; 2 3 /** 4 * 單例 5 * 6 * @author turbo 7 * 8 * 2016年9月12日 9 */ 10 public class Singleton { 11 private static Singleton instance; 12 13 private Singleton() { 14 } 15 16 public static Singleton GetInstance() { 17 18 if (instance == null) { 19 synchronized (Singleton.class) { 20 if (instance == null){ 21 instance = new Singleton(); 22 } 23 } 24 } 25 26 return instance; 27 } 28 }
利用雙重鎖的方式這樣顆粒度變小了,但還是利用同步的方式來解決資源共享問題。其實這上面兩種寫法稱之為“懶加載”,即在用到的時候再來實例化。
我們再次修改代碼,如下。
1 package day_5_singleton; 2 3 /** 4 * 單例 5 * 6 * @author turbo 7 * 8 * 2016年9月12日 9 */ 10 public class Singleton { 11 private static Singleton instance = new Singleton(); 12 13 private Singleton() { 14 } 15 16 public static Singleton GetInstance() { 17 return instance; 18 } 19 }
我們不利用線程同步的方式,而是在類被加載的時候就生成一個實例對象。這稱之為“勤加載”,這個帶來的問題就是,不管這個單例有沒有用到都會一直存在。
兩者都有其優缺點,但相對於利用線程同步的方式來解決線程安全問題,“勤加載”會是一個較為明智的選擇。
2016.9.16補充:之所以懶加載不采取任何措施造成的線程不安全問題,是因為在程序中出現了“競態條件(Race Condition)”,由於不恰當的執行時序而出現不正確的結果。最常見的競態條件類型就是“先檢查后執行(Check-Then-Act)”操作,即通過一個可能失效的觀測結果來決定下一步的動作。——《Java 並發編程實戰》