1.什么是單例模式?
《Head First 設計模式》中給出如下定義:確保一個類只有一個實例,並提供一個全局訪問點。
關鍵詞:唯一實例對象。
2.單例模式的實現方式:
2.1 懶漢式
對於實例做懶加載處理,即在客戶第一次使用時再做創建,所以第一次獲取實例的效率會稍微低一些。
1 /** 2 * 懶漢式 3 * @author Lsj 4 */ 5 public class LazySingleton { 6 7 private static LazySingleton instance; 8 9 /** 10 * 獲取單一實例對象—非同步方法 11 * @return 12 */ 13 public static LazySingleton getInstance(){ 14 if(instance == null){ 15 try { 16 TimeUnit.NANOSECONDS.sleep(1);//為了使模擬效果更直觀,這里延時1ms,具體看時序圖 17 } catch (InterruptedException e) { 18 // TODO Auto-generated catch block 19 e.printStackTrace(); 20 } 21 instance = new LazySingleton(); 22 } 23 return instance; 24 } 25 26 }
這種創建方式可以延遲加載、但是在多線程環境下獲取到的實例可能並非唯一的,具體見如下驗證:

1 import java.util.concurrent.CountDownLatch; 2 import java.util.concurrent.TimeUnit; 3 4 /** 5 * 懶漢式 6 * @author Lsj 7 */ 8 public class LazySingleton { 9 10 private static LazySingleton instance; 11 12 /** 13 * 獲取單一實例對象—非同步方法 14 * @return 15 */ 16 public static LazySingleton getInstance(){ 17 if(instance == null){ 18 try { 19 TimeUnit.NANOSECONDS.sleep(1);//為了使模擬效果更直觀,這里延時1ms,具體看時序圖 20 } catch (InterruptedException e) { 21 // TODO Auto-generated catch block 22 e.printStackTrace(); 23 } 24 instance = new LazySingleton(); 25 } 26 return instance; 27 } 28 29 /** 30 * 增加同步鎖 31 * 避免多線程環境下並發產生多個實例可能的同時,會帶來性能上的損耗。 32 * 事實上只有第一次創建時需要這么做,但后續依然通過加鎖獲取單例對象就有點因小失大了。 33 * @return 34 */ 35 public synchronized static LazySingleton getInstanceSyn(){ 36 if(instance == null){ 37 try { 38 TimeUnit.MILLISECONDS.sleep(1);//為了使模擬效果更直觀,這里延時1ms,具體看時序圖 39 } catch (InterruptedException e) { 40 // TODO Auto-generated catch block 41 e.printStackTrace(); 42 } 43 instance = new LazySingleton(); 44 } 45 return instance; 46 } 47 48 public static void main(String[] args) throws InterruptedException { 49 //模擬下多線程環境下實例可能不唯一的情況 50 CountDownLatch startSignal = new CountDownLatch(1); 51 for(int i=0;i<2;i++){//模擬2個線程 52 Thread t = new Thread(new MyThread(startSignal)); 53 t.setName("thread " + i); 54 t.start(); 55 } 56 Thread.sleep(1000); 57 startSignal.countDown(); 58 } 59 60 } 61 62 class MyThread implements Runnable { 63 64 private final CountDownLatch startSignal; 65 66 public MyThread(CountDownLatch startSignal){ 67 this.startSignal = startSignal; 68 } 69 70 public void run() { 71 try { 72 System.out.println("current thread : " + Thread.currentThread().getName() + " is waiting."); 73 startSignal.await(); 74 } catch (InterruptedException e) { 75 // TODO Auto-generated catch block 76 e.printStackTrace(); 77 } 78 LazySingleton l = LazySingleton.getInstance(); 79 System.out.println(l); 80 } 81 82 }
從驗證結果可以看出兩個線程同時獲取實例時,得到的並非同一個實例對象:
2.2 懶漢式(+同步鎖)
1 public synchronized static LazySingleton getInstanceSyn(){ 2 if(instance == null){ 3 instance = new LazySingleton(); 4 } 5 return instance; 6 }
在上述懶漢式的獲取對象方法上做出一些改變,給獲取實例的方法加上synchronized同步鎖, 但是這種做法在多次調用獲取實例方法的情況下會帶來性能上的損耗。
事實上只有第一次創建實例時需要這么做,但后續依然通過加鎖獲取單例對象就有點因小失大了。
2.3 餓漢式
顧名思義、在類加載時就將唯一實例一並加載,后續只需要獲取就可以了。
1 /** 2 * 餓漢式 3 * @author Lsj 4 */ 5 public class HungrySingleton { 6 7 private static final String CLASS_NAME = "HungrySingleton"; 8 9 private static final HungrySingleton instance = new HungrySingleton(); 10 11 static{ 12 System.out.println("類加載時創建:"+instance);//這里可以看到類加載后,優先加載上方的靜態成員變量 13 } 14 15 private HungrySingleton(){ 16 17 } 18 19 public static HungrySingleton getInstance(){ 20 return instance; 21 } 22 23 public static void main(String[] args) throws ClassNotFoundException { 24 System.out.println(HungrySingleton.CLASS_NAME);//可以看到,這里僅僅是打印HungrySingleton的靜態常量,但實例依然被初始化了。 25 System.out.println("==========分割線=========="); 26 HungrySingleton instance1 = HungrySingleton.getInstance(); 27 System.out.println(instance1); 28 HungrySingleton instance2 = HungrySingleton.getInstance(); 29 System.out.println(instance2); 30 } 31 32 }
運行結果:
從結果可以看到,這種獲取單例的方式是線程安全的,JVM保障在多線程情況下一定先創建此實例並且只做一次實例化處理,但是這種情況沒有做到懶加載,比如只是引用此類中的一個靜態成員變量(常量),此實例在類加載時也一起被初始化了,如果后續應用中不使用這個對象,則會造成資源浪費,占用內存。
2.4 雙重檢查加鎖
此方式可以看做是在懶漢式(+同步鎖)方式上的進一步提升,從代碼上可以看出主要是針對創建的過程加同步鎖。
1 /** 2 * 通過雙重檢查的方式創建及獲取單例對象 3 * @author Lsj 4 */ 5 public class DoubleCheckedLockingSingleton { 6 7 private volatile static DoubleCheckedLockingSingleton instance; 8 9 private DoubleCheckedLockingSingleton(){} 10 11 public static DoubleCheckedLockingSingleton getInstance(){ 12 if(instance == null){ 13 synchronized (DoubleCheckedLockingSingleton.class) { 14 if(instance == null){ 15 instance = new DoubleCheckedLockingSingleton(); 16 } 17 } 18 } 19 return instance; 20 } 21 22 }
這種方式可以大大減少2.2方法中獲取實例時不必要的同步操作,需要注意的是:靜態成員變量中定義的volatile關鍵字,保證線程間變量的可見性以及防止指令重排序,同時需要的注意必須是jdk1.5及以上版本(volatile關鍵字在1.5做出了增強處理)。
--這里后續補充不加volatile關鍵字的危害。
2.5 靜態內部類
此方案是基於類初始化的解決方案,JVM在類的初始化階段,會執行類的初始化。在執行類的初始化期間,JVM會去獲取一個鎖。這個鎖可以同步多個線程對同一個類的初始化。
1 /** 2 * 以靜態內部類的方式來創建獲取單例對象 3 * @author Lsj 4 * 5 */ 6 public class InnerSingleton { 7 8 private InnerSingleton(){ 9 } 10 11 public static InnerSingleton getInstance(){ 12 return InnerSingleton.InnerClass.instance; 13 } 14 15 static class InnerClass { 16 private static InnerSingleton instance = new InnerSingleton(); 17 } 18 19 }
這種方式受益於JVM在多線程環境下對於類初始化的同步控制,這里不再做太詳細的說明。
2.6 通過枚舉實現單例
《Effective Java》一書中作者Joshua Bloch提倡使用這種方式,這種方式依然是依靠JVM保障,而且可以防止反序列化的時候創建新的對象。
1 /** 2 * 通過枚舉實現單例模式 3 * @author Lsj 4 * 5 */ 6 public class EnumSingleton { 7 8 public static EnumSingleton getInstance(){ 9 return Singleton.INSTANCE.getInstance(); 10 } 11 12 enum Singleton { 13 INSTANCE; 14 private EnumSingleton instance; 15 16 private Singleton(){ 17 instance = new EnumSingleton(); 18 } 19 20 private EnumSingleton getInstance(){ 21 return instance; 22 } 23 } 24 25 }
筆者沒有這么使用過,暫不做過多描述、實際工作中也很少看到有這么用的,待后續有深入了解后再補充。
3. 總結:
幾種單例模式的實現方式中,建議使用4,5,6這三種方式,實際根據使用場景作出選擇,另外對於上述提到的幾種方式1~5,需要防范通過反射或反序列化的手段創建對象從而使得實例不再唯一,筆者也會在后續會對此作出補充。
參考文獻:
《Head Frist設計模式》
《Effective Java》
《Java並發編程的藝術》