您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關Java中實現單例模式的法有哪些,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
一、餓漢設計模式
public class SingletonHungry { private final static SingletonHungry INSTANCE = new SingletonHungry(); private SingletonHungry() { } public static SingletonHungry getInstance() { return INSTANCE; } }
因為單例對象一開始就初始化了,不會出現線程安全的問題。
PS:因為我們只需要初始化1次,所以給INSTANCE加了final
關鍵字,表明初始化1次后不再允許初始化。
懶漢單例設計模式
由于餓漢模式一開始就初始化好了,但如果一直沒有被使用到的話,是會浪費珍貴的內存資源的,所以引出了懶漢模式。
懶漢:首次使用時才會去實例化對象。
public class SingletonLazy1 { private static SingletonLazy1 instance; private SingletonLazy1() { } public static SingletonLazy1 getInstance() { if (instance == null) { instance = new SingletonLazy1(); } return instance; } }
測試:
public class Main { public static void main(String[] args) { SingletonLazy1 instance1 = SingletonLazy1.getInstance(); SingletonLazy1 instance2 = SingletonLazy1.getInstance(); System.out.println(instance1); System.out.println(instance2); } }
測試結果:從結果可以看出,打印出來的兩個實例對象地址是一樣的,所以認為是只創建了一個對象。
1:解決多線程并發問題
上述代碼存在的問題:在多線程環境下,不能保證只創建一個實例,我們進行問題的重現:
public class Main { public static void main(String[] args) { new Thread(()-> System.out.println(SingletonLazy1.getInstance())).start(); new Thread(()-> System.out.println(SingletonLazy1.getInstance())).start(); } }
結果:獲取到的對象不一樣,這并不是我們的預期結果。
解決方案:
public class SingletonLazy2 { private static SingletonLazy2 instance; private SingletonLazy2() { } //在方法加synchronized修飾符 public static synchronized SingletonLazy2 getInstance() { if (instance == null) { instance = new SingletonLazy2(); } return instance; } }
測試:
public class Main2 { public static void main(String[] args) { new Thread(()-> System.out.println(SingletonLazy2.getInstance())).start(); new Thread(()-> System.out.println(SingletonLazy2.getInstance())).start(); new Thread(()-> System.out.println(SingletonLazy2.getInstance())).start(); new Thread(()-> System.out.println(SingletonLazy2.getInstance())).start(); } }
結果:多線程環境下獲取到的是同個對象。
上一方案雖然解決了多線程問題,但由于synchronized關鍵字是加在方法上的,鎖粒度很大,當有上萬甚至更多的線程同時訪問時,都被攔在了方法外,大大降低了程序性能,所以我們要適當縮小鎖粒度,控制鎖的范圍在代碼塊上。
public class SingletonLazy3 { private static SingletonLazy3 instance; private SingletonLazy3() { } public static SingletonLazy3 getInstance() { //代碼塊1:不要在if外加鎖,不然和鎖方法沒什么區別 if (instance == null) { //代碼塊2:加鎖,將方法鎖改為鎖代碼塊 synchronized (SingletonLazy3.class) { //代碼塊3 instance = new SingletonLazy3(); } } return instance; } }
測試:
public class Main3 { public static void main(String[] args) { new Thread(()-> System.out.println(SingletonLazy3.getInstance())).start(); new Thread(()-> System.out.println(SingletonLazy3.getInstance())).start(); new Thread(()-> System.out.println(SingletonLazy3.getInstance())).start(); new Thread(()-> System.out.println(SingletonLazy3.getInstance())).start(); } }
我們看一下運行結果:還是出現了線程安全的問題(每次執行都可能打印不同的地址情況,只要證明是非線程安全的即可)。
原因分析:當線程A拿到鎖進入到
代碼塊3
并且還沒有創建完實例時,線程B是有機會到達代碼塊2
的,此時線程C和D可能在代碼塊1
,當線程A執行完之后釋放鎖并返回對象1,線程B進入進入代碼塊3
,又創建了新的對象2覆蓋對象1并返回,最后當線程C和D在進行判null時發現instance非空,直接返回最后創建的對象2。
所謂雙重檢查鎖,就是在線程獲取到鎖之后再對實例進行第2次判空檢查,判斷是不是有上一個線程已經進行了實例化,有的話直接返回即可,否則進行實例初始化。
public class SingletonLazy4DCL { private static SingletonLazy4DCL instance; private SingletonLazy4DCL() { } public static SingletonLazy4DCL getInstance() { //代碼塊1:第一次判空檢查 if (instance == null) { //代碼塊2:加鎖,將方法鎖改為鎖代碼塊 synchronized (SingletonLazy3.class) { //代碼塊3:進行第二次(雙重)判空檢查 if (instance == null) { instance = new SingletonLazy4DCL(); } } } return instance; } }
測試:
public class Main4DCL { public static void main(String[] args) { new Thread(()-> System.out.println(SingletonLazy4DCL.getInstance())).start(); new Thread(()-> System.out.println(SingletonLazy4DCL.getInstance())).start(); new Thread(()-> System.out.println(SingletonLazy4DCL.getInstance())).start(); new Thread(()-> System.out.println(SingletonLazy4DCL.getInstance())).start(); } }
在對象的實例過程中,大概可分為以下3個步驟:
分配對象內存空間
在空間中創建對象
實例指向分配到的內存空間地址
由于實例化對象的過程不是原子性的,且JVM本身對Java代碼指令有重排的操作,可能1-2-3的操作被重新排序成了1-3-2,這樣就會導致在3執行完之后還沒來得及創建對象時,其他線程先讀取到了未初始化的對象instance并提前返回,在使用的時候會出現NPE空指針異常。
解決:給instance加volatile
關鍵字表明禁止指令重排,出現的概率不大, 但這是更安全的一種做法。
public class SingletonLazy5Volatile { //加volatile關鍵字 private volatile static SingletonLazy5Volatile instance; private SingletonLazy5Volatile() { } public static SingletonLazy5Volatile getInstance() { //代碼塊1 if (instance == null) { //代碼塊2:加鎖,將方法鎖改為鎖代碼塊 synchronized (SingletonLazy3.class) { //代碼塊3 if (instance == null) { instance = new SingletonLazy5Volatile(); } } } return instance; } }
我們還可以使用靜態類的靜態變量被第一次訪問時才會進行初始化的特性來進行懶加載初始化。把外部類的單例對象放到靜態內部類的靜態成員變量里進行初始化。
public class SingletonLazy6InnerStaticClass { private SingletonLazy6InnerStaticClass() { } public static SingletonLazy6InnerStaticClass getInstance() { return SingletonLazy6InnerStaticClass.InnerStaticClass.instance; //或者寫成return InnerStaticClass.instance; } private static class InnerStaticClass { private static final SingletonLazy6InnerStaticClass instance = new SingletonLazy6InnerStaticClass(); } }
雖然靜態內部類里的寫法和餓漢模式很像,但它卻不是在外部類加載時就初始化了,而是在第一次被訪問到時才會進行初始化的操作(即getInstance方法被調用時),也就起到了懶加載的效果,并且它可以保證線程安全。
測試:
public class Main6InnerStatic { public static void main(String[] args) { new Thread(()-> System.out.println(SingletonLazy6InnerStaticClass.getInstance())).start(); new Thread(()-> System.out.println(SingletonLazy6InnerStaticClass.getInstance())).start(); new Thread(()-> System.out.println(SingletonLazy6InnerStaticClass.getInstance())).start(); new Thread(()-> System.out.println(SingletonLazy6InnerStaticClass.getInstance())).start(); } }
反射攻擊
雖然我們一開始都對構造器進行了私有化處理,但Java本身的反射機制卻還是可以將private訪問權限改為可訪問,依舊可以創建出新的實例對象,這里以餓漢模式舉例說明:
public class MainReflectAttack { public static void main(String[] args) { try { SingletonHungry normal1 = SingletonHungry.getInstance(); SingletonHungry normal2 = SingletonHungry.getInstance(); //開始反射創建實例 Constructor<SingletonHungry> reflect = SingletonHungry.class.getDeclaredConstructor(null); reflect.setAccessible(true); SingletonHungry attack = reflect.newInstance(); System.out.println("正常靜態方法調用獲取到的對象:"); System.out.println(normal1); System.out.println(normal2); System.out.println("反射獲取到的對象:"); System.out.println(attack); } catch (Exception e) { e.printStackTrace(); } } }
public enum SingletonEnum { INSTANCE; }
枚舉是最簡潔、線程安全、不會被反射創建實例的單例實現,《Effective Java》中也表明了這種寫法是最佳的單例實現模式。
單元素的枚舉類型經常成為實現Singleton的最佳方法。 --《Effective Java》
為什么說不會被反射創建對象呢?查閱構造器反射實例化對象方法newInstance
的源碼可知:反射禁止了枚舉對象的實例化,也就防止了反射攻擊,不用自己在構造器實現復雜的重復實例化邏輯了。
測試:
public class MainEnum { public static void main(String[] args) { SingletonEnum instance1 = SingletonEnum.INSTANCE; SingletonEnum instance2 = SingletonEnum.INSTANCE; System.out.println(instance1.hashCode()); System.out.println(instance2.hashCode()); } }
總結:幾種實現方式的優缺點 懶漢模式
優點:節省內存。
缺點:存在線程安全問題,若要保證線程安全,則寫法復雜。
餓漢模式
優點:線程安全。
缺點:如果單例對象一直沒被使用,則會浪費內存空間。
靜態內部類
優點:懶加載并避免了多線程問題,寫法相比于懶漢模式更簡單。
缺點:需要多創建一個內部類。
枚舉
優點:簡潔、天生線程安全、不可反射創建實例。
缺點:暫無
關于Java中實現單例模式的法有哪些就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。