1. 程式人生 > >如何創建一個完美的單例模式

如何創建一個完美的單例模式

第一個 單例 現在 連接池 解決 write [] struct not

單例模式的目的是什麽

單例類的目的是控制對象創建,約束對象的數量有且只有一個。單例模式只允許有一個入口來創建類的實例。

因為只有一個單例類的實例,任何單例類的實例都將之會產生一個類,就像靜態域。當你需要控制資源的時候,如何數據庫連接池、線程池或者使用sockets,單例模式是非常有用的。

下面我們來創建一個單例類。

創建單例類

為了實現單例類,最簡單的方式就是將構造器私有化設置為private。有兩種初始化方式

餓漢式

餓漢式初始化,單例類的實例在類加載的時候被創建,這是創建單例類最簡單的方法。

通過將構造器聲明為private,不允許其他類來創建單例類實例。取而代之的是,創建一個靜態方法(一般命名為getInstance)來提供創建類實例的唯一入口。

 1  package com.net;
 2 /**
 3  * Created by admin on 2017/9/28.
 4  */
 5 public class SingletonClass {
 6     private static SingletonClass singletonClass = new SingletonClass();
 7     //private constructor
 8     private SingletonClass(){
 9 
10     }
11     public static SingletonClass getInstance(){
12 return singletonClass; 13 } 14 15 }

這種方法有一個缺陷,就是中程序沒有使用到它的時候,實例已經被創建了。當你創建數據庫連接或者socket的時候,這可能成為一個相當打的問題,會導致內存泄漏問題。解決的方法是,在需要的時候

再創建實例,我們稱為懶漢式初始化。

懶漢式

與餓漢式相反,你在getInstance()方法中初始化類實例。方法中判斷實例是否已經創建,如果已經創建,則返回舊的實例,反之在JVM中創建新的實例並返回。

 1 package com.net;
 2 
 3 /**
 4  * Created by admin on 2017/9/28.
5 */ 6 public class SingletonClass { 7 private static SingletonClass singletonClass = null; 8 9 private SingletonClass(){ 10 11 } 12 13 public static SingletonClass getInstance(){ 14 if (singletonClass == null){ 15 singletonClass = new SingletonClass(); 16 } 17 return singletonClass; 18 } 19 20 }

我們都知道這JAVA中,如果兩個對象是相同的,那麽他們的hashCode也是相同的。我們測試一下,如果上面的單例類都正確,那麽他們應該會返回相同的哈希值。

 1 package com.net.test;
 2 
 3 import com.net.SingletonClass;
 4 
 5 /**
 6  * Created by admin on 2017/9/28.
 7  */
 8 public class Test {
 9     public static void main(String[] args) {
10         SingletonClass singletonClass1 = SingletonClass.getInstance();
11         SingletonClass singletonClass2 = SingletonClass.getInstance();
12         System.out.println("singletonClass1hashcode:"+singletonClass1.hashCode()); 
13 System.out.println("singletonClass2hashcode:"+singletonClass1.hashCode());
14 }
15 }

輸出日誌:

singletonClass1hashcode:21029277
singletonClass2hashcode:21029277

可以看到兩個實例擁有相同的的hashcode。所以,這就意味著上面的代碼創建了一個完美的單例類,是嗎?回單是no

讓單例類反射安全

在上面的單例類中,通過反射可以創建不止一個實例。Java Reflection 是一個在運行時檢測或者修改類的運行時行為的過程。通過在運行時修改構造器的可見性並通過構造器創建實例可以產生新的單例實例。

如下代碼:

 1 import java.lang.reflect.Constructor;
 2 import java.lang.reflect.InvocationTargetException;
 3 
 4 /**
 5  * Created by admin on 2017/9/28.
 6  */
 7 public class Test {
 8     public static void main(String[] args) {
 9         SingletonClass singletonClass1 = SingletonClass.getInstance();
10         SingletonClass singletonClass2 = null;
11 
12         try {
13             Class<SingletonClass> clazz = SingletonClass.class;
14             Constructor<SingletonClass> cons = clazz.getDeclaredConstructor();
15             cons.setAccessible(true);
16                 singletonClass2 = cons.newInstance();
17         } catch (Exception e) {
18             e.printStackTrace();
19         }
20         System.out.println("singletonClass1hashcode:"+singletonClass1.hashCode()); 
21     System.out.println("singletonClass2hashcode:"+singletonClass2.hashCode());
22 }
23 }

輸入日誌:

singletonClass1hashcode:21029277
singletonClass2hashcode:24324022

可以看到每一個實例都有不同的hashCode。顯然這個單例類是不合格的。

解決方案:

為了防止反射導致的單例失敗,當構造器已經初始化並且其他類再次初始化時,拋出一個運行時異常。下面是更新後的代碼:

 1 package com.net;
 2 
 3 /**
 4  * Created by admin on 2017/9/28.
 5  */
 6 public class SingletonClass {
 7     private static SingletonClass singletonClass = null;
 8 
 9     private  SingletonClass(){
10         if(singletonClass != null){
11             throw new RuntimeException("Use getInstance method to get the single instance of this class");
12         }
13     }
14 
15     public  static  SingletonClass getInstance(){
16         if (singletonClass == null){
17             singletonClass = new SingletonClass();
18         }
19         return singletonClass;
20     }
21 
22 }

讓單例類線程安全

如果兩個線程幾乎同時去嘗試初始化單例類,將會發生什麽?測試下面代碼

 1 import com.net.SingletonClass;
 2 
 3 import java.lang.reflect.Constructor;
 4 import java.lang.reflect.InvocationTargetException;
 5 
 6 /**
 7  * Created by admin on 2017/9/28.
 8  */
 9 public class Test {
10     public static void main(String[] args) {
11         Thread thread1 = new Thread(new Runnable() {
12             @Override
13             public void run() {
14                 SingletonClass singletonClass1 = SingletonClass.getInstance();
15                 System.out.println("singletonClass1 hashcode:"+singletonClass1);
16             }
17         });
18         Thread thread2 = new Thread(new Runnable() {
19             @Override
20             public void run() {
21                 SingletonClass singletonClass2 = SingletonClass.getInstance();
22                 System.out.println("singletonClass2 hashcode:"+singletonClass2);
23             }
24         });
25         thread1.start();
26         thread2.start();
27 
28     }
29 }

如果你多次運行這些代碼,有時候你會發現不同的線程出現了不同的實例。如下:

singletonClass2 hashcode:21029277
singletonClass1 hashcode:24324022


這說明了你的單例類並不是線程安全的。所有的線程同時調用getInstance()方法時,singletonClass==null條件對所有的線程返回值,所以兩個不同的實例被創建出來。這就打破了單例的原則。

解決方案

同步getInstance()方法

 1 package com.net;
 2 
 3 /**
 4  * Created by admin on 2017/9/28.
 5  */
 6 public class SingletonClass {
 7     private static SingletonClass singletonClass = null;
 8 
 9     private  SingletonClass(){
10         if(singletonClass != null){
11             throw new RuntimeException("Use getInstance method to get the single instance of this class");
12         }
13     }
14 
15     public synchronized static  SingletonClass getInstance(){
16         if (singletonClass == null){
17             singletonClass = new SingletonClass();
18         }
19         return singletonClass;
20     }
21 
22 }

在外面同步getInstance()方法之後,第二個線程必須等到第一個線程執行完getInstance()方法之後才能執行,這就保證了線程安全。

但是,這個方法同樣有一些缺點:

鎖的開銷導致運行變慢

實例變量初始化之後的同步操作是不必要的雙檢查鎖

使用雙檢查鎖 方法創建實例可以克服上面的問題。

這種方法中,當實例為空時,中同步代碼塊中創建實例,這樣只有當singletonClass為空的時候,同步代碼塊才會執行,避免了不必要的同步操作。

 1 package com.net;
 2 
 3 /**
 4  * Created by admin on 2017/9/28.
 5  */
 6 public class SingletonClass {
 7     private static SingletonClass singletonClass = null;
 8 
 9     private  SingletonClass(){
10         if(singletonClass != null){
11             throw new RuntimeException("Use getInstance method to get the single instance of this class");
12         }
13     }
14 
15     public  static  SingletonClass getInstance(){
16         
17         if (singletonClass == null){ //check for the first time
18             synchronized (SingletonClass.class){
19               if(singletonClass ==null){//check for the second time
20               鎖 = new SingletonClass();
21               }
22             }
23         }
24         return singletonClass;
25     }
26 
27 }

使用volatile關鍵字

表面上看,這個方法看起來很完美,你只需要付出一次靜態代碼塊的代價。但是除非你使用volatile關鍵字,否則單例仍然會被打破。

沒有volatile修飾符,另一個線程可能在變量 singletonClass正在初始化尚未完成時引用它。但是通過volatile的保證happens-before關系,所有對於singletonClass變量的寫操作都會在讀操作之前發生。

 1 package com.net;
 2 
 3 /**
 4  * Created by admin on 2017/9/28.
 5  */
 6 public class SingletonClass {
 7     private static volatile SingletonClass singletonClass = null;
 8 
 9     private  SingletonClass(){
10         if(singletonClass != null){
11             throw new RuntimeException("Use getInstance method to get the single instance of this class");
12         }
13     }
14 
15     public  static  SingletonClass getInstance(){
16 
17         if (singletonClass == null){ //check for the first time
18             synchronized (SingletonClass.class){
19               if(singletonClass ==null){//check for the second time
20               singletonClass = new SingletonClass();
21               }
22             }
23         }
24         return singletonClass;
25     }
26 
27 }

現在上面的單例類是線程安全的。在多數線程應用環境中保證單例類的線程安全是必須的。

讓單例類序列化安全

在分布式的系統中,有些情況下你需要做單例類中實現Serralizable接口。這樣你可以在文件系統中存儲它的狀態並且在稍後的某一事件點取出。

讓我們測試一個這個單例類中序列化和反序列化之後是否仍然保持單例。

 1 package com.net.test;
 2 
 3 import com.net.SingletonClass;
 4 
 5 import java.io.*;
 6 import java.lang.reflect.Constructor;
 7 import java.lang.reflect.InvocationTargetException;
 8 
 9 /**
10  * Created by admin on 2017/9/28.
11  */
12 public class Test {
13     public static void main(String[] args) {
14         try {
15             SingletonClass singletonClass1 = SingletonClass.getInstance();
16             ObjectOutput out = null;
17             out = new ObjectOutputStream(new FileOutputStream("filename.cc"));
18             out.writeObject(singletonClass1);
19             out.close();
20             //deserialize from file to object
21             ObjectInput in = new ObjectInputStream(new FileInputStream("filename.cc"));
22              SingletonClass singletonClass2 = (SingletonClass)in.readObject();
23              in.close();
24             System.out.println("singletonClass1hashcode:"+singletonClass1.hashCode());
25             System.out.println("singletonClass2hashcode:"+singletonClass2.hashCode());
26         } catch (IOException e) {
27             e.printStackTrace();
28         } catch (ClassNotFoundException e) {
29             e.printStackTrace();
30         }
31 
32     }
33 }

輸出結果:

singletonClass1hashcode:23050916
singletonClass2hashcode:9286386

顯然看到實例的hashcode是不同的,違反了單例原則。序列化單例類後,我們反序列化時,會創建一個新的類實例。為了預防另一個實例的產生,你需要提供readResolve()方法的實現。readResolve()

代替了從流中讀取對象。這就確保了中序列化和反序列化的過程中沒有人可以創建新的實例。

 1 package com.net;
 2 
 3 import java.io.Serializable;
 4 
 5 /**
 6  * Created by admin on 2017/9/28.
 7  */
 8 public class SingletonClass implements Serializable{
 9     private static volatile SingletonClass singletonClass = null;
10 
11     private  SingletonClass(){
12         if(singletonClass != null){
13             throw new RuntimeException("Use getInstance method to get the single instance of this class");
14         }
15     }
16 
17     public  static  SingletonClass getInstance(){
18 
19         if (singletonClass == null){ //check for the first time
20             synchronized (SingletonClass.class){
21               if(singletonClass ==null){//check for the second time
22               singletonClass = new SingletonClass();
23               }
24             }
25         }
26         return singletonClass;
27     }
28     //make singleton from serialize and deserialize operation
29     protected Object readResolve(){
30         return getInstance();
31     }
32 }


如何創建一個完美的單例模式