java單例模式實現

1.最基本的單例模式

/** * @author LearnAndGet * @time 2018年11月13日 * 最基本的單例模式 */
public class SingletonV1 { private static SingletonV1 instance = new SingletonV1();; //構造函數私有化
    private SingletonV1() {} public static SingletonV1 getInstance() { return instance; } }
import org.junit.Test; public class SingletonTest { @Test public void test01() throws Exception { SingletonV1 s1 = SingletonV1.getInstance(); SingletonV1 s2 = SingletonV1.getInstance(); System.out.println(s1.hashCode()); System.out.println(s2.hashCode()); } } //運行結果以下:
589873731
589873731

2.類加載時不初始化實例的模式

  上述單例模式在類加載的時候,就會生成實例,可能形成空間浪費,若是須要修改爲,在須要使用時才生成實例,則可修改代碼以下:html

 1 public class SingletonV2 {  2     
 3     private static SingletonV2 instance;  4     
 5     //構造函數私有化
 6     private SingletonV2() {}  7 
 8     public static SingletonV2 getInstance(){ 10         if(instance == null) 11  { 12             instance = new SingletonV2(); 13  } 14         return instance; 15  } 16 }

 

然而,上述方案雖然在類加載時不會生成實例,可是存在線程安全問題,若是線程A在執行到第10行時,線程B也進入該代碼塊,剛好也執行好第10行,此時若是實例還沒有生成,則線程A和線程B都會執行第12行的代碼,各自生成一個實例,此時就違背了單例模式的設計原則。實際測試代碼以下:java

public class SingletonTest { @Test public void test02() throws Exception { for(int i=0;i<1000;i++) { Thread th1 = new getInstanceThread(); th1.start(); } } class getInstanceThread extends Thread { public void run() { try { SingletonV2 s = SingletonV2.getInstance(); System.out.println(Thread.currentThread().getName()+" get Instance "+s.hashCode()+" Time: "+System.currentTimeMillis()); }catch(Exception e) { e.printStackTrace(); } } } }

通過屢次測試,可能產生以下輸出結果:安全

  

3.線程安全的單例模式

  在上述單例模式下進行改進,在getInstance方法前加入 Sychronized關鍵字,來實現線程安全,修改後代碼以下:併發

 1 public class SingletonV3 {  2     
 3     private static SingletonV3 instance;  4     
 5     //構造函數私有化
 6     private SingletonV3() {}  7 
    //synchronized關鍵字在靜態方法上,鎖定的是當前類:sychronized關鍵字 8 public static synchronized SingletonV3 getInstance() 9 { 10 if(instance == null) 11 { 12 instance = new SingletonV3(); 13 } 14 return instance; 15 } 16 }

 增長sychronized關鍵字後,確實可以改善線程安全問題,可是也帶來了額外的鎖開銷。性能受到必定影響。舉例來講,此時若是有1000個線程都須要使用SingletonV3實例,由於加鎖的位置在getInstance上,所以,每一個線程都必須等待其餘獲取了鎖的線程徹底執行完鎖中的方法後,纔可以進入該方法並獲取本身的實例。eclipse

 

 

4.雙重校檢+線程安全單例模式

  因而能夠在上述代碼的基礎上,只有當Singleton實例未被初始化時,對實例化方法加鎖便可。在Singleton實例已經被初始化時,無需加鎖,直接返回當前Singleton對象。代碼以下:函數

 1     private static SingletonV4 instance;  2     
 3     //構造函數私有化
 4     private SingletonV4() {}  5 
 6     public static SingletonV4 getInstance()  7  {  8         if(instance == null)  9  { 10             synchronized(SingletonV4.class) 11  { 12                 //雙重校檢
13                 if(instance == null) 14  { 15                     instance = new SingletonV4(); 16  } 17  } 18  } 19         return instance; 20     }

 

5.內部類單例模式

  儘管上述方案解決了同步問題,雙重校檢也使得性能開銷大大減少,可是,只有有synchronized關鍵字的存在。性能多多少少仍是會有一些影響,此時,咱們想到了 "內部類"的用法。性能

  ①.內部類不會隨着類的加載而加載測試

  ②.一個類被加載,當且僅當其某個靜態成員(靜態域、構造器、靜態方法等)被調用時發生。lua

  靜態內部類隨着方法調用而被加載,只加載一次,不存在併發問題,因此是線程安全。基於此,修改代碼以下:spa

  

 /推薦指數:★★★★★
 
 
 1 public class SingletonV5 {  2     //構造函數私有化
 3     private SingletonV5() {}  4 
 5     static class SingetonGet  6  {  7         private static final SingletonV5 instance = new SingletonV5();  8  }  9     
10     public static SingletonV5 getInstance() 11  { 12         return SingetonGet.instance; 13  } 14 }

6.反射都不能破壞的單例模式

  靜態內部類實現的單例模式,是目前比較推薦的方式,可是在java功能強大反射的機制下,它就是個弟弟,此時利用反射仍然可以建立出多個實例,如下是建立實例的代碼:

  

 1  @Test  2     public void test4()  3  {  4         //普通方式獲取實例s1,s2
 5         SingletonV5 s1 = SingletonV5.getInstance();  6         SingletonV5 s2 = SingletonV5.getInstance();  7         //利用反射獲取實例s3,s4
 8         SingletonV5 s3 = null;  9         SingletonV5 s4 = null; 10         try 
11  { 12             Class<SingletonV5> clazz = SingletonV5.class; 13             Constructor<SingletonV5> constructor = clazz.getDeclaredConstructor(); 14             constructor.setAccessible(true); 15             s3 = constructor.newInstance(); 16             s4 = constructor.newInstance(); 17         }catch(Exception e) 18  { 19  e.printStackTrace(); 20  } 21         
22  System.out.println(s1.hashCode()); 23  System.out.println(s2.hashCode()); 24  System.out.println(s3.hashCode()); 25  System.out.println(s4.hashCode()); 26     }

輸出結果以下:

  

589873731
589873731
200006406
2052001577

 能夠看到,s1和s2擁有相同的哈希碼,所以他們是同一個實例,可是s三、s4,是經過反射後用構造函數從新構造生成的實例,他們均與s1,s2不一樣。此時單例模式下產生了多個不一樣的對象,違反了設計原則。

基於上述反射可能形成的單例模式失效,考慮在私有的構造函數中添加是否初始化的標記位,使私有構造方法只可能被執行一次。

 

public class SingletonV6 { //是否已經初始化過的標記位
    private static boolean isInitialized = false; //構造函數中,當實例已經被初始化時,不能繼續獲取新實例
    private SingletonV6() { synchronized(SingletonV6.class) { if(isInitialized == false) { isInitialized = !isInitialized; }else { throw new RuntimeException("單例模式被破壞..."); } } } static class SingetonGet { private static final SingletonV6 instance = new SingletonV6(); } public static SingletonV6 getInstance() { return SingetonGet.instance; } }

測試代碼以下:

 @Test public void test5() { SingletonV6 s1 = SingletonV6.getInstance(); SingletonV6 s2 = null; try { Class<SingletonV6> clazz = SingletonV6.class; Constructor<SingletonV6> constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); s2 = constructor.newInstance(); }catch(Exception e) { e.printStackTrace(); } System.out.println(s1.hashCode()); System.out.println(s2.hashCode()); }

  運行上述代碼時,會拋出異常:

  

java.lang.reflect.InvocationTargetException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source) at java.lang.reflect.Constructor.newInstance(Unknown Source) at SingletonTest.SingletonTest.test5(SingletonTest.java:98) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86) at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:538) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:760) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:460) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:206) Caused by: java.lang.RuntimeException: 單例模式被破壞... at SingletonTest.SingletonV6.<init>(SingletonV6.java:26) ... 28 more 2052001577

 

7.序列化反序列化都不能破壞的單例模式

  通過上述改進,反射也不可以破壞單例模式了。可是,依然存在一種可能形成上述單例模式產生兩個不一樣的實例,那就是序列化。當一個對象A通過序列化,而後再反序列化,獲取到的對象B和A是不是同一個實例呢,驗證代碼以下:

  

/** * @Author {LearnAndGet} * @Time 2018年11月13日 * @Discription:測試序列化並反序列化是否仍是同一對象 */
package SingletonTest; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInput; import java.io.ObjectInputStream; import java.io.ObjectOutput; import java.io.ObjectOutputStream; public class Main { /** * @param args */
    public static void main(String[] args) { // TODO Auto-generated method stub
        SingletonV6 s1 = SingletonV6.getInstance(); ObjectOutput objOut = null; try { //將s1序列化(記得將Singleton實現Serializable接口)
            objOut = new ObjectOutputStream(new FileOutputStream("c:\\a.objFile")); objOut.writeObject(s1); objOut.close(); //反序列化獲得s2
            ObjectInput objIn = new ObjectInputStream(new FileInputStream("c:\\a.objFile")); SingletonV6 s2 = (SingletonV6) objIn.readObject(); objIn.close(); System.out.println(s1.hashCode()); System.out.println(s2.hashCode()); } catch (Exception e) { // TODO Auto-generated catch block
 e.printStackTrace(); } } }

 

  輸出結果以下:

  

1118140819
990368553

 可見,此時序列化前的對象s1和通過序列化->反序列化步驟後的到的對象s2,並非同一個對象,所以,出現了兩個實例,再次違背了單例模式的設計原則。

爲了消除問題,在單例模式類中,實現Serializable接口以後 添加對readResolve()方法的實現:當從I/O流中讀取對象時,readResolve()方法都會被調用到。實際上就是用readResolve()中返回的對象直接替換在反序列化過程當中建立的對象,而被建立的對象則會被垃圾回收掉。這就確保了在序列化和反序列化的過程當中沒人能夠建立新的實例,修改後的代碼以下:

  

package SingletonTest; import java.io.Serializable; /** * @author LearnAndGet * * @time 2018年11月13日 * */
public class SingletonV6  implements Serializable{ //是否已經初始化過的標記位
    private static boolean isInitialized = false; //構造函數中,當實例已經被初始化時,不能繼續獲取新實例
    private SingletonV6() { synchronized(SingletonV6.class) { if(isInitialized == false) { isInitialized = !isInitialized; }else { throw new RuntimeException("單例模式被破壞..."); } } } static class SingetonGet { private static final SingletonV6 instance = new SingletonV6(); } public static SingletonV6 getInstance() { return SingetonGet.instance; } //實現readResolve方法
    private Object readResolve() { return getInstance(); } }

 從新運行上述序列化和反序列過程,能夠發現,此時獲得的對象是同一對象。

  

1118140819
1118140819

 

8.總結

  在實際開發中,根據本身的須要,選擇對應的單例模式便可,不同非要實現第7節中那種無堅不摧的單例模式。畢竟不是全部場景下都須要實現序列化接口, 也並非全部人都會用反射來破壞單例模式。所以比較經常使用的是第5節中的,內部類單例模式,代碼簡潔明瞭,且節省空間。

相關文章
相關標籤/搜索