在23個標準設計模式中,單例模式在應用中仍是很常見的,可是在多線程環境中,單例模式的使用有很是多的坑,使用好單例模式的一個原則:如何使單例模式在遇到多線程的環境中是安全的、正確的。下面分析幾種多線程的實現方式以及遇到的坑。設計模式
當即加載:實用類的時候已經將對象建立完畢,常見的是直接new實例化,有「着急」,「急迫」的意思,所以也稱:「餓漢模式」。在調用方法前,已經實例化對象。代碼以下:安全
單例模式:多線程
public class SingleTon01 { private static SingleTon01 instance=new SingleTon01(); public SingleTon01() { super(); } public static SingleTon01 getInstance(){ return instance; } }
線程:ide
public class MyThread extends Thread{ @Override public void run() { System.out.println(SingleTon01.getInstance().hashCode()); } }
測試類:測試
public class Run { public static void main(String[] args) { MyThread m1=new MyThread(); MyThread m2=new MyThread(); MyThread m3=new MyThread(); MyThread m4=new MyThread(); m1.start(); m2.start(); m3.start(); m4.start(); } }
運行結果:優化
全部線程的對象hashCode均是同樣的,證實是單例模式,but,該代碼的實現是優缺點的:不能有其餘實例變量,由於getInstance方法沒有同步,可能會出現線程安全問題。spa
延遲加載:在調用方法的時候,對象才被實例化,經常使用的實現方式就是在方法內部實例化對象。代碼以下:線程
單例模式:設計
public class SingleTon02 { private static SingleTon02 instance; public SingleTon02() { super(); } public static SingleTon02 getInstance(){ if(null==instance){ instance=new SingleTon02(); } return instance; } }
線程類:3d
public class MyThread extends Thread{ @Override public void run() { System.out.println(SingleTon02.getInstance().hashCode()); } }
測試類:
public class Run { public static void main(String[] args) { MyThread m1=new MyThread(); MyThread m2=new MyThread(); MyThread m3=new MyThread(); MyThread m4=new MyThread(); m1.start(); m2.start(); m3.start(); m4.start(); } }
運行結果:
從運行結果來看,控制檯打印了多個hashCode值,說明該實現方式在多線程的環境中是失敗的,如何解決呢?其實很簡單,讓方法同步便可,使用synchronized關鍵字。改進後代碼吐下:
public class SingleTon02 { private static SingleTon02 instance; public SingleTon02() { super(); } synchronized public static SingleTon02 getInstance(){ try { if(null==instance){ Thread.sleep(3000);//模擬業務處理 instance=new SingleTon02(); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } return instance; } }
再次運行:
同步以後,證實該單例模式是正確的。可是,這種方式又帶來一種缺點,那就是效率問題,由於下一個線程必須須要等上一個線程釋放鎖以後才能執行,須要排隊執行,所以還能夠優化,那就是:嘗試同步代碼塊,針對重要代碼進行單獨同步,以提高效率。
下面總結了一種使用DCL雙檢查鎖機制實現單例模式,該模式適用於在多線程環境中的延遲加載單例模式設計。代碼以下:
public class SingleTon03 { private volatile static SingleTon03 instance; public SingleTon03() { super(); } public static SingleTon03 getInstance(){ try { if(null==instance){ Thread.sleep(3000);//模擬業務處理 synchronized (SingleTon03.class) { if(null==instance){ instance=new SingleTon03(); } } } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } return instance; } }
這種方式既保證了線程的安全性,還保證了效率。
前面的改進方式能夠實如今多線程的環境中實現單例模式,而且保證線程安全,那麼這種靜態內置類的方式也能夠實現一樣的效果。建立靜態內類,以下:
public class SingleTon04 { private static class SingleInner{ private static SingleTon04 instance=new SingleTon04(); } public SingleTon04() { super(); } public static SingleTon04 getInstance(){ return SingleInner.instance; } }
線程類:
public class MyThread extends Thread{ @Override public void run() { System.out.println(SingleTon04.getInstance().hashCode()); } }
測試類同上
運行結果:
靜態內置類當然能夠實現單例模式,可是這裏有一個坑,那就是在遇到序列化和反序列化的時候,依然會出現問題,依然會出現多個實例化對象,代碼以下:
單例模式
public class SingleTon05 implements Serializable{ private static final long serialVersionUID = 888888L; //內部類方式 private static class SingleTonInner{ private static final SingleTon05 instance=new SingleTon05(); } public SingleTon05() { super(); } public static SingleTon05 getInstance(){ return SingleTonInner.instance; } }
序列化運行類:
public class Run2 { public static void main(String[] args) { //寫 try { SingleTon05 singleTon05=SingleTon05.getInstance(); FileOutputStream out=new FileOutputStream(new File("singleton05.txt")); ObjectOutputStream objectOutputStream=new ObjectOutputStream(out); objectOutputStream.writeObject(singleTon05); objectOutputStream.close(); out.close(); //打印hashcode System.out.println(singleTon05.hashCode()); } catch (IOException e) { e.printStackTrace(); } //讀 try { FileInputStream in=new FileInputStream(new File("singleton05.txt")); ObjectInputStream objectInputStream=new ObjectInputStream(in); SingleTon05 singleTon05=(SingleTon05)objectInputStream.readObject(); objectInputStream.close(); in.close(); //打印hashcode System.out.println(singleTon05.hashCode()); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } }
運行結果:
很明顯,寫入和讀出來的對象不是一個,顯然不符合單例模式的設計模式。序列化破壞了單例模式,固然,還有一種破壞單例模式的方式,那就是反射,單例模式中儘可能不要使用反射。呢麼問題來了,如何改進呢,其實很簡單,在序列化的時候在調用一個方法。改進以下:
public class SingleTon05 implements Serializable{ private static final long serialVersionUID = 888888L; //內部類方式 private static class SingleTonInner{ private static final SingleTon05 instance=new SingleTon05(); } public SingleTon05() { super(); } public static SingleTon05 getInstance(){ return SingleTonInner.instance; } protected Object readResolve()throws ObjectStreamException { System.out.println("調用了readResolve方法!"); return SingleTonInner.instance; } }
再次運行:
序列化操做提供了一個很特別的鉤子(hook)-類中具備一個私有的被實例化的方法readresolve(),這個方法能夠確保類的開發人員在序列化將會返回怎樣的object上具備發言權。這樣就確保咱們在反序列化的時候返回的對象是同一個。
靜態代碼塊中的代碼執行實在實用類的時候加載,所以咱們能夠應用靜態代碼塊的這種特性來設計單例模式。代碼以下:
public class SingleTon06{ private static SingleTon06 instance=null; public SingleTon06() { super(); } static{ instance=new SingleTon06(); } public static SingleTon06 getInstance(){ return instance; } }
線程類測試類同三,結果以下:
由於枚舉和靜態代碼塊的特性有類似之處,所以也可使用這種特性來設計單例模式,這種模式很是簡單,也推薦時使用。代碼以下:
public enum SingleTon07{ INSTANCE; private SingleTon07() { } public static SingleTon07 getInstance(){ return INSTANCE; } }
測試運行類同上,結果以下:
特色就是實現很是簡單。