摘要:單例模式的懶漢實現線程安全通過設置同步方法,效率太低,整個方法被加鎖模擬在創建對象之前做一些準備工作使用上面的測試類,測試結果可以看到,這種方式達到了線程安全。可以說這種方式是實現單例模式的最優解。
1. 什么是單例模式
單例模式指的是在應用整個生命周期內只能存在一個實例。單例模式是一種被廣泛使用的設計模式。他有很多好處,能夠避免實例對象的重復創建,減少創建實例的系統開銷,節省內存。
2. 單例模式和靜態類的區別首先理解一下什么是靜態類,靜態類就是一個類里面都是靜態方法和靜態field,構造器被private修飾,因此不能被實例化。Math類就是一個靜態類。
知道了什么是靜態類后,來說一下他們兩者之間的區別:
1)首先單例模式會提供給你一個全局唯一的對象,靜態類只是提供給你很多靜態方法,這些方法不用創建對象,通過類就可以直接調用;
2)如果是一個非常重的對象,單例模式可以懶加載,靜態類就無法做到;
那么時候時候應該用靜態類,什么時候應該用單例模式呢?首先如果你只是想使用一些工具方法,那么最好用靜態類,靜態類比單例類更快,因為靜態的綁定是在編譯期進行的。如果你要維護狀態信息,或者訪問資源時,應該選用單例模式。還可以這樣說,當你需要面向對象的能力時(比如繼承、多態)時,選用單例類,當你僅僅是提供一些方法時選用靜態類。
3.如何實現單例模式1. 餓漢模式
所謂餓漢模式就是立即加載,一般情況下再調用getInstancef方法之前就已經產生了實例,也就是在類加載的時候已經產生了。這種模式的缺點很明顯,就是占用資源,當單例類很大的時候,其實我們是想使用的時候再產生實例。因此這種方式適合占用資源少,在初始化的時候就會被用到的類。
class SingletonHungary { private static SingletonHungary singletonHungary = new SingletonHungary(); //將構造器設置為private禁止通過new進行實例化 private SingletonHungary() { } public static SingletonHungary getInstance() { return singletonHungary; } }
2. 懶漢模式
懶漢模式就是延遲加載,也叫懶加載。在程序需要用到的時候再創建實例,這樣保證了內存不會被浪費。針對懶漢模式,這里給出了5種實現方式,有些實現方式是線程不安全的,也就是說在多線程并發的環境下可能出現資源同步問題。
首先第一種方式,在單線程下沒問題,在多線程下就出現問題了。
// 單例模式的懶漢實現1--線程不安全 class SingletonLazy1 { private static SingletonLazy1 singletonLazy; private SingletonLazy1() { } public static SingletonLazy1 getInstance() { if (null == singletonLazy) { try { // 模擬在創建對象之前做一些準備工作 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } singletonLazy = new SingletonLazy1(); } return singletonLazy; } }
我們模擬10個異步線程測試一下:
public class SingletonLazyTest { public static void main(String[] args) { Thread2[] ThreadArr = new Thread2[10]; for (int i = 0; i < ThreadArr.length; i++) { ThreadArr[i] = new Thread2(); ThreadArr[i].start(); } } } // 測試線程 class Thread2 extends Thread { @Override public void run() { System.out.println(SingletonLazy1.getInstance().hashCode()); } }
運行結果:
124191239 124191239 872096466 1603289047 1698032342 1913667618 371739364 124191239 1723650563 367137303
可以看到他們的hashCode不都是一樣的,說明在多線程環境下,產生了多個對象,不符合單例模式的要求。
那么如何使線程安全呢?第二種方法,我們使用synchronized關鍵字對getInstance方法進行同步。
// 單例模式的懶漢實現2--線程安全 // 通過設置同步方法,效率太低,整個方法被加鎖 class SingletonLazy2 { private static SingletonLazy2 singletonLazy; private SingletonLazy2() { } public static synchronized SingletonLazy2 getInstance() { try { if (null == singletonLazy) { // 模擬在創建對象之前做一些準備工作 Thread.sleep(1000); singletonLazy = new SingletonLazy2(); } } catch (InterruptedException e) { e.printStackTrace(); } return singletonLazy; } }
使用上面的測試類,測試結果:
1210004989 1210004989 1210004989 1210004989 1210004989 1210004989 1210004989 1210004989 1210004989 1210004989
可以看到,這種方式達到了線程安全。但是缺點就是效率太低,是同步運行的,下個線程想要取得對象,就必須要等上一個線程釋放,才可以繼續執行。
那我們可以不對方法加鎖,而是將里面的代碼加鎖,也可以實現線程安全。但這種方式和同步方法一樣,也是同步運行的,效率也很低。
// 單例模式的懶漢實現3--線程安全 // 通過設置同步代碼塊,效率也太低,整個代碼塊被加鎖 class SingletonLazy3 { private static SingletonLazy3 singletonLazy; private SingletonLazy3() { } public static SingletonLazy3 getInstance() { try { synchronized (SingletonLazy3.class) { if (null == singletonLazy) { // 模擬在創建對象之前做一些準備工作 Thread.sleep(1000); singletonLazy = new SingletonLazy3(); } } } catch (InterruptedException e) { // TODO: handle exception } return singletonLazy; } }
我們來繼續優化代碼,我們只給創建對象的代碼進行加鎖,但是這樣能保證線程安全么?
// 單例模式的懶漢實現4--線程不安全 // 通過設置同步代碼塊,只同步創建實例的代碼 // 但是還是有線程安全問題 class SingletonLazy4 { private static SingletonLazy4 singletonLazy; private SingletonLazy4() { } public static SingletonLazy4 getInstance() { try { if (null == singletonLazy) { //代碼1 // 模擬在創建對象之前做一些準備工作 Thread.sleep(1000); synchronized (SingletonLazy4.class) { singletonLazy = new SingletonLazy4(); //代碼2 } } } catch (InterruptedException e) { // TODO: handle exception } return singletonLazy; } }
我們來看一下運行結果:
1210004989 1425839054 1723650563 389001266 1356914048 389001266 1560241484 278778395 124191239 367137303
從結果看來,這種方式不能保證線程安全,為什么呢?我們假設有兩個線程A和B同時走到了‘代碼1’,因為此時對象還是空的,所以都能進到方法里面,線程A首先搶到鎖,創建了對象。釋放鎖后線程B拿到了鎖也會走到‘代碼2’,也創建了一個對象,因此多線程環境下就不能保證單例了。
讓我們來繼續優化一下,既然上述方式存在問題,那我們在同步代碼塊里面再一次做一下null判斷不就行了,這種方式就是我們的DCL雙重檢查鎖機制。
//單例模式的懶漢實現5--線程安全 //通過設置同步代碼塊,使用DCL雙檢查鎖機制 //使用雙檢查鎖機制成功的解決了單例模式的懶漢實現的線程不安全問題和效率問題 //DCL 也是大多數多線程結合單例模式使用的解決方案 class SingletonLazy5 { private static SingletonLazy5 singletonLazy; private SingletonLazy5() { } public static SingletonLazy5 getInstance() { try { if (null == singletonLazy) { // 模擬在創建對象之前做一些準備工作 Thread.sleep(1000); synchronized (SingletonLazy5.class) { if(null == singletonLazy) { singletonLazy = new SingletonLazy5(); } } } } catch (InterruptedException e) { // TODO: handle exception } return singletonLazy; } }
運行結果:
124191239 124191239 124191239 124191239 124191239 124191239 124191239 124191239 124191239 124191239
我們可以看到DCL雙重檢查鎖機制很好的解決了懶加載單例模式的效率問題和線程安全問題。這也是我們最常用到的方式。
3. 靜態內部類
我們也可以使用靜態內部類實現單例模式,代碼如下:
//使用靜態內部類實現單例模式--線程安全 class SingletonStaticInner { private SingletonStaticInner() { } private static class SingletonInner { private static SingletonStaticInner singletonStaticInner = new SingletonStaticInner(); } public static SingletonStaticInner getInstance() { try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } return SingletonInner.singletonStaticInner; } }
可以看到使用這種方式我們沒有顯式的進行任何同步操作,那他是如何保證線程安全呢?和餓漢模式一樣,是靠JVM保證類的靜態成員只能被加載一次的特點,這樣就從JVM層面保證了只會有一個實例對象。那么問題來了,這種方式和餓漢模式又有什么區別呢?不也是立即加載么?實則不然,加載一個類時,其內部類不會同時被加載。一個類被加載,當且僅當其某個靜態成員(靜態域、構造器、靜態方法等)被調用時發生。
可以說這種方式是實現單例模式的最優解。
4. 靜態代碼塊
這里提供了靜態代碼塊實現單例模式。這種方式和第一種類似,也是一種餓漢模式。
//使用靜態代碼塊實現單例模式 class SingletonStaticBlock { private static SingletonStaticBlock singletonStaticBlock; static { singletonStaticBlock = new SingletonStaticBlock(); } public static SingletonStaticBlock getInstance() { return singletonStaticBlock; } }
5. 序列化與反序列化
LZ為什么要提序列化和反序列化呢?因為單例模式雖然能保證線程安全,但在序列化和反序列化的情況下會出現生成多個對象的情況。運行下面的測試類,
public class SingletonStaticInnerSerializeTest { public static void main(String[] args) { try { SingletonStaticInnerSerialize serialize = SingletonStaticInnerSerialize.getInstance(); System.out.println(serialize.hashCode()); //序列化 FileOutputStream fo = new FileOutputStream("tem"); ObjectOutputStream oo = new ObjectOutputStream(fo); oo.writeObject(serialize); oo.close(); fo.close(); //反序列化 FileInputStream fi = new FileInputStream("tem"); ObjectInputStream oi = new ObjectInputStream(fi); SingletonStaticInnerSerialize serialize2 = (SingletonStaticInnerSerialize) oi.readObject(); oi.close(); fi.close(); System.out.println(serialize2.hashCode()); } catch (Exception e) { e.printStackTrace(); } } } //使用匿名內部類實現單例模式,在遇見序列化和反序列化的場景,得到的不是同一個實例 //解決這個問題是在序列化的時候使用readResolve方法,即去掉注釋的部分 class SingletonStaticInnerSerialize implements Serializable { /** * 2018年03月28日 */ private static final long serialVersionUID = 1L; private static class InnerClass { private static SingletonStaticInnerSerialize singletonStaticInnerSerialize = new SingletonStaticInnerSerialize(); } public static SingletonStaticInnerSerialize getInstance() { return InnerClass.singletonStaticInnerSerialize; } // protected Object readResolve() { // System.out.println("調用了readResolve方法"); // return InnerClass.singletonStaticInnerSerialize; // } }
可以看到:
865113938 1078694789
結果表明的確是兩個不同的對象實例,違背了單例模式,那么如何解決這個問題呢?解決辦法就是在反序列化中使用readResolve()方法,將上面的注釋代碼去掉,再次運行:
865113938 調用了readResolve方法 865113938
問題來了,readResolve()方法到底是何方神圣,其實當JVM從內存中反序列化地"組裝"一個新對象時,就會自動調用這個 readResolve方法來返回我們指定好的對象了, 單例規則也就得到了保證。readResolve()的出現允許程序員自行控制通過反序列化得到的對象。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/71069.html
摘要:缺點每次調用都有線程開銷延遲初始化單例默認構造方法為,避免用戶用構造出新對象獲取單例的靜態工廠同步方法延遲初始化單例使用同步方法保證多線程操作只實例化一個實力單例模式。 主要分為兩種: 直接初始化 延遲初始化 直接初始化 直接初始化final靜態成員 線程安全:JVM保證final靜態成員只會被初始化一次 公有靜態成員是個final域,直接引用成員獲取單例 /** * 公有靜態成...
摘要:總結我們主要介紹到了以下幾種方式實現單例模式餓漢方式線程安全懶漢式非線程安全和關鍵字線程安全版本懶漢式雙重檢查加鎖版本枚舉方式參考設計模式中文版第二版設計模式深入理解單例模式我是一個以架構師為年之內目標的小小白。 初遇設計模式在上個寒假,當時把每個設計模式過了一遍,對設計模式有了一個最初級的了解。這個學期借了幾本設計模式的書籍看,聽了老師的設計模式課,對設計模式算是有個更進一步的認識。...
摘要:使用靜態類體現的是基于對象,而使用單例設計模式體現的是面向對象。二編寫單例模式的代碼編寫單例模式的代碼其實很簡單,就分了三步將構造函數私有化在類的內部創建實例提供獲取唯一實例的方法餓漢式根據上面的步驟,我們就可以輕松完成創建單例對象了。 前言 只有光頭才能變強 回顧前面: 給女朋友講解什么是代理模式 包裝模式就是這么簡單啦 本來打算沒那么快更新的,這陣子在刷Spring的書籍。在看...
摘要:不同于個人面經,這份面經具有普適性。我在前面的文章中也提到了應該怎么做自我介紹與項目介紹,詳情可以查看這篇文章備戰春招秋招系列初出茅廬的程序員該如何準備面試。是建立連接時使用的握手信號。它表示確認發來的數據已經接受無誤。 showImg(https://segmentfault.com/img/remote/1460000016972448?w=921&h=532); 該文已加入開源文...
摘要:單例模式單例模式,也叫單子模式,是一種常用的軟件設計模式。在應用這個模式時,單例對象的類必須保證只有一個實例存在。透明的單例模式利用一個形成一個閉包,在里邊通過變量來記錄實例,并返回構造函數。 單例模式 單例模式,也叫單子模式,是一種常用的軟件設計模式。在應用這個模式時,單例對象的類必須保證只有一個實例存在。 ———來自維基百科 一個很典型的應用是在點擊登錄按鈕,彈出登錄浮窗,不論...
閱讀 3982·2021-10-09 09:43
閱讀 2883·2021-10-08 10:05
閱讀 2746·2021-09-08 10:44
閱讀 892·2019-08-30 15:52
閱讀 2827·2019-08-26 17:01
閱讀 3027·2019-08-26 13:54
閱讀 1659·2019-08-26 10:48
閱讀 817·2019-08-23 14:41