單列模式與多執行緒
在23個標準設計模式中,單例模式在應用中還是很常見的,但是在多執行緒環境中,單例模式的使用有非常多的坑,使用好單例模式的一個原則:如何使單例模式在遇到多執行緒的環境中是安全的、正確的。下面分析幾種多執行緒的實現方式以及遇到的坑。
一、立即載入/餓漢模式
立即載入:實用類的時候已經將物件建立完畢,常見的是直接new例項化,有“著急”,“急迫”的意思,因此也稱:“餓漢模式”。在呼叫方法前,已經例項化物件。程式碼如下:
單例模式:
public class SingleTon01 { private static SingleTon01 instance=new SingleTon01(); public SingleTon01() { super(); } public static SingleTon01 getInstance(){ return instance; } }
執行緒:
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方法沒有同步,可能會出現執行緒安全問題。
二、延遲載入/懶漢模式
延遲載入:在呼叫方法的時候,物件才被例項化,常用的實現方式就是在方法內部例項化物件。程式碼如下:
單例模式:
public class SingleTon02 { private static SingleTon02 instance; public SingleTon02() { super(); } public static SingleTon02 getInstance(){ if(null==instance){ instance=new SingleTon02(); } return instance; } }
執行緒類:
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; } }
測試執行類同上,結果如下:
特點就是實現非常簡單。