本文只須要考慮一件事:如何使單例模式遇到多線程是安全的、正確的html
什麼是當即加載?當即加載就是使用類的時候已經將對象建立完畢,常見的實現辦法就是直接 new 實例化。編程
public class MyObject { private static MyObject myObject = new MyObject(); public MyObject(){ } public static MyObject getInstance(){ return myObject; } public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } }
打印結果:設計模式
985396398 985396398 985396398
控制檯打印的 hashCode 是同一個值,說明對象是同一個,也就實現了當即加載型單例設計模式。安全
此版本的缺點是不能有其餘其餘實例變量,由於getInstance()方法沒有同步,因此有可能出現非線程安全問題。多線程
什麼是延遲加載?延遲加載就是在調用 get() 方法時實例才被建立,常見的實現方法就是在 get() 方法中進行 new() 實例化。併發
測試代碼:ide
public class MyObject { private static MyObject myObject; public MyObject() { } public static MyObject getInstance() { try { if (myObject == null) { //模擬對象在建立以前作的一些準備工做 Thread.sleep(3000); myObject = new MyObject(); } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; } public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } }
打印結果:測試
985396398 610025186 21895028
從運行結果來看,建立了三個對象,並非真正的單例模式。緣由顯而易見,3個線程同時進入了if (myObject == null)
判斷語句中,最後各自都建立了對象。.net
既然多個線程能夠同時進入getInstance() 方法,那麼只須要對其進行同步synchronized處理便可。線程
public class MyObject { private static MyObject myObject; public MyObject() { } synchronized public static MyObject getInstance() { try { if (myObject == null) { //模擬對象在建立以前作的一些準備工做 Thread.sleep(3000); myObject = new MyObject(); } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; } public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } }
打印結果:
961745937 961745937 961745937
雖然運行結果代表,成功實現了單例,但這種給整個方法上鎖的解決方法效率過低。
同步方法是對方法的總體加鎖,這對運行效率來說很不利的。改爲同步代碼塊後:
public class MyObject { private static MyObject myObject; public MyObject() { } public static MyObject getInstance() { try { synchronized (MyObject.class) { if (myObject == null) { //模擬對象在建立以前作的一些準備工做 Thread.sleep(3000); myObject = new MyObject(); } } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; } public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } }
打印結果:
355159803 355159803 355159803
運行結果雖然代表是正確的,但同步synchronized語句塊依舊把整個 getInstance()方法代碼包括在內,和synchronize 同步方法效率是同樣低下。
因此,咱們能夠針對某些重要的代碼進行單獨的同步,而其餘的代碼則不須要同步。這樣在運行時,效率徹底能夠獲得大幅度提高。
public class MyObject { private static MyObject myObject; public MyObject() { } public static MyObject getInstance() { try { if (myObject == null) { //模擬對象在建立以前作的一些準備工做 Thread.sleep(3000); synchronized (MyObject.class) { myObject = new MyObject(); } } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; } public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } }
運行結果:
985396398 21895028 610025186
此方法只對實例化對象的關鍵代碼進行同步,從語句的結構上來講,運行的效率的確獲得的提高。可是在多線程的狀況下依舊沒法解決獲得一個單例對象的結果。
在最後的步驟中,使用DCL雙檢查鎖機制來實現多線程環境中的延遲加載單例設計模式。
public class MyObject { private volatile static MyObject myObject; public MyObject() { } public static MyObject getInstance() { try { if (myObject == null) { //模擬對象在建立以前作的一些準備工做 Thread.sleep(3000); synchronized (MyObject.class) { if (myObject == null) { myObject = new MyObject(); } } } } catch (InterruptedException e) { e.printStackTrace(); } return myObject; } public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } }
運行結果:
860826410 860826410 860826410
使用DCL雙重檢查鎖功能,成功地解決了「懶漢模式」遇到多線程的問題。DCL也是大多數多線程結合單例模式使用的解決方案。
volatile 在此處的做用是:
1)可見性:保證了不一樣線程對這個變量進行操做時的可見性,即一個線程修改了某個變量的值,這新值對其餘線程來講是當即可見的。
2)有序性:禁止進行指令重排序。當程序執行到volatile變量的讀操做或者寫操做時,在其前面的操做的更改確定所有已經進行,在其後面的操做確定尚未進行。
擴展閱讀:關於volatile解決DCL(雙重檢查)問題的見解
DCL能夠解決多線程單例模式的非線程安全問題。固然,還有許多其它的方法也能達到一樣的效果。
public class MyObject { public static class MyObjectHandle{ private static MyObject myObject = new MyObject(); public static MyObject getInstance() { return myObject; } } public static MyObject getInstance(){ return MyObjectHandle.getInstance(); } public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } }
打印結果:
1035057739 1035057739 1035057739
靜態內置類能夠達到線程安全問題,但若是遇到序列化對象時,使用默認的方式運行獲得的結果仍是多例的。
解決方法就是在反序列化中使用readResolve()方法:
public class MyObject implements Serializable { //靜態內部類 public static class MyObjectHandle{ private static final MyObject myObject = new MyObject(); } public static MyObject getInstance(){ return MyObjectHandle.myObject; } protected Object readResolve(){ System.out.println("調用了readResolve方法"); return MyObjectHandle.myObject; } public static void main(String[] args) throws IOException, ClassNotFoundException { MyObject myObject = MyObject.getInstance(); FileOutputStream outputStream = new FileOutputStream(new File("myObject.txt")); ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); objectOutputStream.writeObject(myObject); objectOutputStream.close(); System.out.println(myObject.hashCode()); FileInputStream inputStream = new FileInputStream(new File("myObject.txt")); ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); MyObject object = (MyObject) objectInputStream.readObject(); objectInputStream.close(); System.out.println(object.hashCode()); } }
運行結果:
621009875 調用了readResolve方法 621009875
靜態代碼塊中的代碼在使用類的時候就已經執行了,因此能夠應用靜態代碼塊的這個特色來實現單例設計模式。
public class MyObject { private static MyObject myObject = null; static { myObject = new MyObject(); } public static MyObject getInstance(){ return myObject; } public static void main(String[] args){ new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } }
運行結果:
355159803 355159803 355159803
枚舉enum 和靜態代碼塊的特性類似,在使用枚舉類時,構造方法會被自動調用,也能夠應用其這個特性實現單例設計模式。
public enum Singleton { INSTANCE; private MyObject myObject = null; Singleton() { myObject = new MyObject(); } public MyObject getInstance(){ return myObject; } public static void main(String[] args){ new Thread(new Runnable() { @Override public void run() { System.out.println(Singleton.INSTANCE.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(Singleton.INSTANCE.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(Singleton.INSTANCE.getInstance().hashCode()); } }).start(); } }
運行結果:
1516133987 1516133987 1516133987
這樣實現的一個弊端就是違反了「職責單一原則」,完善後的代碼以下:
public class MyObject { public enum Singleton { INSTANCE; private MyObject myObject = null; Singleton() { myObject = new MyObject(); } public MyObject getInstance() { return myObject; } } public static MyObject getInstance(){ return Singleton.INSTANCE.getInstance(); } public static void main(String[] args){ new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println(MyObject.getInstance().hashCode()); } }).start(); } }
運行結果:
610025186 610025186 610025186
本文使用若干案例來闡述單例模式與多線程結合遇到的狀況與解決方案。
《Java多線程編程核心技術》高洪巖著