Java-單例模式
單例模式是我們實際開發中常用到的開發模式,目的是保證例項的唯一性,確保這個類在記憶體中只會存在一個物件, 但我們現在用到的單例模式相關程式碼可能不是最優的,今天讓我們探索一下單例模式的正確寫法。
單例模式通常分為餓漢式和懶漢式,我們這裡來一個最簡單的程式碼:
餓漢式相關程式碼:
public class SingletonPattern { //無參構造私有化,不允許直接new獲得例項 private SingletonPattern() { } //建立靜態啊私有例項 private static SingletonPattern hungerSingleton =new SingletonPattern(); //同過公共靜態方法獲取例項,確保唯一 public static SingletonPattern getHungerInstance(){ return hungerSingleton; } }
缺點:
提前建立類例項,無論是否需要,只要類載入就進行了例項化,浪費資源.
懶漢式相關程式碼:
//懶漢式, private static SingletonPattern lazySingleton; public static SingletonPattern getLazyInstance(){ //判斷如果沒有例項建立,則建立例項 if(lazySingleton == null){ lazySingleton =new SingletonPattern(); } return lazySingleton; }
缺點:
需要的時候建立類例項,沒有考慮到多執行緒,多執行緒環境下無法保證單例效果,會多次執行SingletonPattern instance=new SingletonPattern()
懶漢式和餓漢式主要區別是否先建立類的例項,一個是拿時間換空間,一個是拿空間換時間,懶漢式只有我需要他的時候才去載入它,懶載入機制,餓漢式不管需不需要我先在記憶體中開闢空間。這兩種是最基本的單列模式,接下來我們就以懶漢式為例,分析如何正確建立和使用單例。
我們分析上面的懶漢式程式碼的問題是:多執行緒情況下無法保證單例,也就是說:如果同時多個執行緒訪問可能會建立多個例項的情況出現.那我們首先想到的是-synchronized,來進行同步方法或同步塊, 程式碼如下:
//懶漢式加鎖同步 private static SingletonPattern syncSingleton; public static SingletonPattern getSyncyInstance(){ if(syncSingleton == null){ //在此處加鎖同步比在方法出加鎖同步縮小了範圍,效能稍高 synchronized (SingletonPattern.class){ syncSingleton =new SingletonPattern(); } } return syncSingleton; }
缺點:
雖然使用了synchronized進行了執行緒的同步,還是會存在多次執行的可能SingletonPattern instance=new SingletonPattern()
進一步優化採用DCL(Double Check Lock)雙重檢查來確定單例模式的執行緒安全和唯一,相關程式碼如下:
//懶漢式雙層檢查 private static SingletonPattern dclSingleton; public static SingletonPattern getDCLInstance(){ if(dclSingleton == null){//第一層檢查 //在此處加鎖同步比在方法出加鎖同步縮小了範圍,效能稍高 synchronized (SingletonPattern.class){ if(dclSingleton == null){//第二層檢查 dclSingleton =new SingletonPattern(); } } } //此處有可能多線返回null物件。導致崩潰 return dclSingleton; }
缺點:
首先我們先了解一下:通常我們進行例項化包含以下幾步:
1.給 Singleton 例項分配記憶體,將函式壓棧,並且申明變數型別;
2.初始化建構函式以及裡面的欄位,在堆記憶體開闢空間;
3.將 instance 物件指向分配的記憶體空間;
java 編譯器允許執行無序,並且 jdk1.5之前不限制處理器重排序,不能保證按序執行,處理器會進行指令重排序優化導致程式崩潰。舉例:正常順序是1-2-3,優化重排後執行順序可能為:1-3-2, 這時假如有 A 和 B 兩條執行緒, A執行緒執行到3的步驟,但是未執行2,這時候 B 執行緒來了搶了許可權,判斷不為空,直接取走 instance可能會造成程式崩潰。
也就是說在jdk1.5之前有兩個問題:
- 執行緒間共享變數不可見性;
-
無序性(執行順序無法保證);
為了解決jdk1.5存在的上述問題,我們需要在一個執行緒進行初始化等操作的時候對其他進入的執行緒可見,執行完畢後,其他執行緒在進行操作,這就又引出一個關鍵字-volatile
相關程式碼:
//volatile+懶漢式雙層檢查(DCL,Double Check Lock) private static volatile SingletonPattern volatileSingleton; public static SingletonPattern getVolatileInstance(){ if(volatileSingleton == null){//第一層檢查 //在此處加鎖同步比在方法出加鎖同步縮小了範圍,效能稍高 synchronized (SingletonPattern.class){ if(volatileSingleton == null){//第二層檢查 volatileSingleton =new SingletonPattern(); } } } return dclSingleton; }
分析:
先說一下volatile關鍵字的兩大作用:
- 可以保證在多執行緒環境下,變數的修改可見性
- 提供記憶體屏障,來保證某些指令順序處理器不能夠優化重排,編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障來禁止特定型別的處理器重排序。
所以使用volatile關鍵字可以保證例項化的賦值操作是最後一步完成,實現了正確的單例模式。
其他單例的實現方法:
- 靜態內部類
-
列舉
採用靜態內部類也是一種不錯的選擇,理由是靜態內部類在沒有顯示呼叫的時候是不會進行載入的,當執行了return 後才載入初始化。
相關程式碼:
public static SingletonPattern InnerSingletonInstance(){ return staticSingleInstance.staticSingleton; } private static class staticSingleInstance{ private static SingletonPattern staticSingleton=new SingletonPattern(); }
列舉類是java1.5才出現的,採用列舉的方式除了寫起來很簡便,還有個好處是安全:因為JVM會保證enum不能被反射並且構造器方法只執行一次,但列舉會很耗記憶體,所以看情況而定吧
相關程式碼如下:
//列舉實現 public static SingletonPattern getEnumInstance(){ return EnumSingleton.INSTANCE.getEnumSingleton(); } privateenum EnumSingleton { INSTANCE; private SingletonPattern enumSingleton; //JVM會保證此方法絕對只調用一次 private SingletonPattern getEnumSingleton() { enumSingleton = new SingletonPattern(); return enumSingleton; } }
關於單例模式的實現,今天就說這麼多,其實核心就是:構造私有,並且通過靜態方法獲取一個例項,在這個過程中必須保證執行緒的安全性。
告辭。