單例模式真是一個老掉牙的問題了,不過我今天是要說些裡面更深點的知識,閑話少說,直接來代碼 1、餓漢式 相信這種寫法大家都知道,一開始接觸單例的時候,大家應該都是用的這種方法: 這種方式優點就是線程安全, 缺點也很明顯,就是類載入的時候,就已實例化該對象了,後面有可能用不到這個實例對象,這樣就會造成空 ...
單例模式真是一個老掉牙的問題了,不過我今天是要說些裡面更深點的知識,閑話少說,直接來代碼
1、餓漢式
相信這種寫法大家都知道,一開始接觸單例的時候,大家應該都是用的這種方法:
package com.hd.single; public class Singleton { private Singleton(){} private static Singleton instance = new Singleton(); public static Singleton getInstance(){ return instance; } }
這種方式優點就是線程安全, 缺點也很明顯,就是類載入的時候,就已實例化該對象了,後面有可能用不到這個實例對象,這樣就會造成空間浪費。因此就有了懶載入方式。
2、懶漢式
1)懶漢式L1
package com.hd.single; public class Singleton2 { private Singleton2(){} private static Singleton2 instance; public static Singleton2 getInstance(){ if(instance == null) //1 instance = new Singleton2(); //2 return instance; } }
這種懶漢式的優點和缺點也很明顯,優點是按需載入,節省空間, 缺點是線程不安全。簡單說就是,有可能線程A執行到“1”處時,阻塞住了,線程B搶到CPU,進來執行並實例化對象,然後線程A醒來後,繼續往下執行,這樣線程A和B取到的就是不同的對象。因此,又有了線程安全的版本。
package com.hd.single; public class Singleton2 { private Singleton2(){} private static Singleton2 instance; public static synchronized Singleton2 getInstance(){ if(instance == null) instance = new Singleton2(); return instance; } }
但是加了synchronized 之後會造成線程阻塞,影響性能。於是又提出了雙檢鎖的方式
1 package com.hd.single; 2 3 public class Singleton2 { 4 5 private Singleton2(){} 6 private static Singleton2 instance; 7 8 public static Singleton2 getInstance(){ 9 if(instance == null){ 10 synchronized (Singleton2.class){ 11 if(instance == null){ 12 instance = new Singleton2(); 13 } 14 } 15 } 16 return instance; 17 } 18 }
看似雙檢鎖的方式很完美,既解決了線程安全的問題,又兼顧了性能問題: 線程先判斷instance變數是否為空,如果不為空,則直接返回。否則進入同步塊去實例化對象。但事實這是一個錯誤的優化!
重點就是第12行代碼(instance = new Singleton2();), 它創建了一個對象。這一行代碼可以分解為如下的3行代碼:
memory = allocate(); //1:分配對象的記憶體空間 ctorInstance(memory); //2:初始化對象 instance = memory; //3:設置instance指向剛分配的記憶體地址
上面2和3這兩步之間,有可能會被重排序,2和3重排序之後的執行時序如下:
memory = allocate(); //1:分配對象的記憶體空間 instance = memory; //3:設置instance指向剛分配的記憶體地址 //註意此時對象還沒有被初始化 ctorInstance(memory); //2:初始化對象
因此如果有線程A執行到3時,此時instance變數確實不為空,然後線程B判斷instance不為空後返回,那麼這是線程B 取到的就是一個空的對象。顯示這樣是有問題的,因此為了防止出現這個問題,需要使用volatile變數,來禁止指令重排序。
2)懶漢式 L2(基於volatile的解決方案)
package com.hd.single; public class Singleton { private Singleton(){} private volatile static Singleton instance; public static Singleton getInstance(){ if(instance == null){ synchronized (Singleton.class){ if(instance == null){ instance = new Singleton3(); } } } return instance; } }
我們除了通過volatile的方式來禁止指令重排序,還可以提供另外一種思路:允許2和3重排序,但不允許其它線程“看到”這個重排序。 前面正是因為線程B看到了重排序,發現instance變數不為空,所以才造成其取到空的對象。
3)懶漢式L3(基於靜態內部類的方案)
package com.hd.single; public class LazySingleton2 { private LazySingleton2() { } static class SingletonHolder { private static final LazySingleton2 instance = new LazySingleton2(); } public static LazySingleton2 getInstance() { return SingletonHolder.instance; } }
因為 在載入外部類時,其內部類不會同時被載入。只有調用 getInstance方法的時候,內部類才會去被載入,且只載入一次,不存在併發問題,因此是線程安全的。
另外,在getInstance()方法中沒有使用synchronized關鍵字,因此沒有造成多餘的性能損耗。
本文給出了多個版本的單例模式,供我們在項目中使用。一般用L2,L3就基本夠用。