<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    隨筆 - 100  文章 - 50  trackbacks - 0
    <2018年11月>
    28293031123
    45678910
    11121314151617
    18192021222324
    2526272829301
    2345678

    常用鏈接

    留言簿(3)

    隨筆分類

    隨筆檔案

    文章分類

    文章檔案

    收藏夾

    我收藏的一些文章!

    搜索

    •  

    最新評論

    閱讀排行榜

    評論排行榜

    1. 前言

     

    單例(Singleton)應(yīng)該是開發(fā)者們最熟悉的設(shè)計模式了,并且好像也是最容易實現(xiàn)的——基本上每個開發(fā)者都能夠隨手寫出——但是,真的是這樣嗎?

     

    作為一個Java開發(fā)者,也許你覺得自己對單例模式的了解已經(jīng)足夠多了。我并不想危言聳聽說一定還有你不知道的——畢竟我自己的了解也的確有限,但究竟你自己了解的程度到底怎樣呢?往下看,我們一起來聊聊看~

     

     

    2. 什么是單例?

     

    單例對象的類必須保證只有一個實例存在——這是維基百科上對單例的定義,這也可以作為對意圖實現(xiàn)單例模式的代碼進(jìn)行檢驗的標(biāo)準(zhǔn)。

     

    對單例的實現(xiàn)可以分為兩大類——懶漢式和餓漢式,他們的區(qū)別在于:

     

    • 懶漢式:指全局的單例實例在第一次被使用時構(gòu)建。

    • 餓漢式:指全局的單例實例在類裝載時構(gòu)建。

     

    從它們的區(qū)別也能看出來,日常我們使用的較多的應(yīng)該是懶漢式的單例,畢竟按需加載才能做到資源的最大化利用嘛~

     

    3. 懶漢式單例

     

    先來看一下懶漢式單例的實現(xiàn)方式。

     

    3.1 簡單版本

     

    看最簡單的寫法Version 1:

    復(fù)制代碼
    // Version 1 public class Single1 {     private static Single1 instance;     public static Single1 getInstance() {         if (instance == null) {             instance = new Single1();         }         return instance;     } }
    復(fù)制代碼

    或者再進(jìn)一步,把構(gòu)造器改為私有的,這樣能夠防止被外部的類調(diào)用。

    復(fù)制代碼
    // Version 1.1 public class Single1 {     private static Single1 instance;     private Single1() {}     public static Single1 getInstance() {         if (instance == null) {             instance = new Single1();         }         return instance;     } }
    復(fù)制代碼

    我仿佛記得當(dāng)初學(xué)校的教科書就是這么教的?—— 每次獲取instance之前先進(jìn)行判斷,如果instance為空就new一個出來,否則就直接返回已存在的instance。

     

    這種寫法在大多數(shù)的時候也是沒問題的。問題在于,當(dāng)多線程工作的時候,如果有多個線程同時運(yùn)行到if (instance == null),都判斷為null,那么兩個線程就各自會創(chuàng)建一個實例——這樣一來,就不是單例了。

     

    3.2 synchronized版本

     

    那既然可能會因為多線程導(dǎo)致問題,那么加上一個同步鎖吧!

     

    修改后的代碼如下,相對于Version1.1,只是在方法簽名上多加了一個synchronized:

    復(fù)制代碼
    // Version 2  public class Single2 {     private static Single2 instance;     private Single2() {}     public static synchronized Single2 getInstance() {         if (instance == null) {             instance = new Single2();         }         return instance;     } }
    復(fù)制代碼

    OK,加上synchronized關(guān)鍵字之后,getInstance方法就會鎖上了。如果有兩個線程(T1、T2)同時執(zhí)行到這個方法時,會有其中一個線程T1獲得同步鎖,得以繼續(xù)執(zhí)行,而另一個線程T2則需要等待,當(dāng)?shù)赥1執(zhí)行完畢getInstance之后(完成了null判斷、對象創(chuàng)建、獲得返回值之后),T2線程才會執(zhí)行執(zhí)行。——所以這端代碼也就避免了Version1中,可能出現(xiàn)因為多線程導(dǎo)致多個實例的情況。

     

    但是,這種寫法也有一個問題:給gitInstance方法加鎖,雖然會避免了可能會出現(xiàn)的多個實例問題,但是會強(qiáng)制除T1之外的所有線程等待,實際上會對程序的執(zhí)行效率造成負(fù)面影響。

     

    3.3 雙重檢查(Double-Check)版本

     

    Version2代碼相對于Version1d代碼的效率問題,其實是為了解決1%幾率的問題,而使用了一個100%出現(xiàn)的防護(hù)盾。那有一個優(yōu)化的思路,就是把100%出現(xiàn)的防護(hù)盾,也改為1%的幾率出現(xiàn),使之只出現(xiàn)在可能會導(dǎo)致多個實例出現(xiàn)的地方。

     

    ——有沒有這樣的方法呢?當(dāng)然是有的,改進(jìn)后的代碼Vsersion3如下:

    復(fù)制代碼
    // Version 3  public class Single3 {     private static Single3 instance;     private Single3() {}     public static Single3 getInstance() {         if (instance == null) {             synchronized (Single3.class) {                 if (instance == null) {                     instance = new Single3();                 }             }         }         return instance;     } }
    復(fù)制代碼

    這個版本的代碼看起來有點復(fù)雜,注意其中有兩次if (instance == null)的判斷,這個叫做『雙重檢查 Double-Check』。

     

    • 第一個if (instance == null),其實是為了解決Version2中的效率問題,只有instance為null的時候,才進(jìn)入synchronized的代碼段——大大減少了幾率。

       

    • 第二個if (instance == null),則是跟Version2一樣,是為了防止可能出現(xiàn)多個實例的情況。

     

    —— 這段代碼看起來已經(jīng)完美無瑕了。

    ……

    ……

    ……

    —— 當(dāng)然,只是『看起來』,還是有小概率出現(xiàn)問題的。

     

    這弄清楚為什么這里可能出現(xiàn)問題,首先,我們需要弄清楚幾個概念:原子操作、指令重排。

     

    知識點:什么是原子操作?

     

    簡單來說,原子操作(atomic)就是不可分割的操作,在計算機(jī)中,就是指不會因為線程調(diào)度被打斷的操作。

     

    比如,簡單的賦值是一個原子操作:

    m = 6; // 這是個原子操作

    假如m原先的值為0,那么對于這個操作,要么執(zhí)行成功m變成了6,要么是沒執(zhí)行m還是0,而不會出現(xiàn)諸如m=3這種中間態(tài)——即使是在并發(fā)的線程中。

     

    而,聲明并賦值就不是一個原子操作:

    int n = 6; // 這不是一個原子操作

    對于這個語句,至少有兩個操作:

     

    ①聲明一個變量n

    ②給n賦值為6

     

    ——這樣就會有一個中間狀態(tài):變量n已經(jīng)被聲明了但是還沒有被賦值的狀態(tài)。

    ——這樣,在多線程中,由于線程執(zhí)行順序的不確定性,如果兩個線程都使用m,就可能會導(dǎo)致不穩(wěn)定的結(jié)果出現(xiàn)。

     

    知識點:什么是指令重排?

     

    簡單來說,就是計算機(jī)為了提高執(zhí)行效率,會做的一些優(yōu)化,在不影響最終結(jié)果的情況下,可能會對一些語句的執(zhí)行順序進(jìn)行調(diào)整。

    比如,這一段代碼:

    復(fù)制代碼
    int a ;   // 語句1   a = 8 ;   // 語句2  int b = 9 ;     // 語句3  int c = a + b ; // 語句4
    復(fù)制代碼

    正常來說,對于順序結(jié)構(gòu),執(zhí)行的順序是自上到下,也即1234。

     

    但是,由于指令重排的原因,因為不影響最終的結(jié)果,所以,實際執(zhí)行的順序可能會變成3124或者1324。

     

    由于語句3和4沒有原子性的問題,語句3和語句4也可能會拆分成原子操作,再重排。

     

    ——也就是說,對于非原子性的操作,在不影響最終結(jié)果的情況下,其拆分成的原子操作可能會被重新排列執(zhí)行順序。

     

    OK,了解了原子操作和指令重排的概念之后,我們再繼續(xù)看Version3代碼的問題。

     

    下面這段話直接從陳皓的文章(深入淺出單實例SINGLETON設(shè)計模式)中復(fù)制而來:

     

    主要在于singleton = new Singleton()這句,這并非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。

     

    1. 給 singleton 分配內(nèi)存

     

    2. 調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量,形成實例

     

    3. 將singleton對象指向分配的內(nèi)存空間(執(zhí)行完這步 singleton才是非 null 了)

    但是在 JVM 的即時編譯器中存在指令重排序的優(yōu)化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執(zhí)行完畢、2 未執(zhí)行之前,被線程二搶占了,這時 instance 已經(jīng)是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然后使用,然后順理成章地報錯。

     

    再稍微解釋一下,就是說,由于有一個『instance已經(jīng)不為null但是仍沒有完成初始化』的中間狀態(tài),而這個時候,如果有其他線程剛好運(yùn)行到第一層if (instance == null)這里,這里讀取到的instance已經(jīng)不為null了,所以就直接把這個中間狀態(tài)的instance拿去用了,就會產(chǎn)生問題。

     

    這里的關(guān)鍵在于——線程T1對instance的寫操作沒有完成,線程T2就執(zhí)行了讀操作。

     

    3.4 終極版本:volatile

     

    對于Version3中可能出現(xiàn)的問題(當(dāng)然這種概率已經(jīng)非常小了,但畢竟還是有的嘛~),解決方案是:只需要給instance的聲明加上volatile關(guān)鍵字即可,Version4版本:

    復(fù)制代碼
    // Version 4   public class Single4 {      private static volatile Single4 instance;      private Single4() {}      public static Single4 getInstance() {          if (instance == null) {              synchronized (Single4.class) {                  if (instance == null) {                      instance = new Single4();                  }              }          }          return instance;      }  }
    復(fù)制代碼

    volatile關(guān)鍵字的一個作用是禁止指令重排,把instance聲明為volatile之后,對它的寫操作就會有一個內(nèi)存屏障(什么是內(nèi)存屏障?),這樣,在它的賦值完成之前,就不用會調(diào)用讀操作。

     

    注意:volatile阻止的不singleton = new Singleton()這句話內(nèi)部[1-2-3]的指令重排,而是保證了在一個寫操作([1-2-3])完成之前,不會調(diào)用讀操作(if (instance == null))。

     

    ——也就徹底防止了Version3中的問題發(fā)生。

    ——好了,現(xiàn)在徹底沒什么問題了吧?

    ……

    ……

    ……

     

    好了,別緊張,的確沒問題了。大名鼎鼎的EventBus中,其入口方法EventBus.getDefault()就是用這種方法來實現(xiàn)的。

    ……

    ……

    ……

    不過,非要挑點刺的話還是能挑出來的,就是這個寫法有些復(fù)雜了,不夠優(yōu)雅、簡潔。

     

    4. 餓漢式單例

     

    下面再聊了解一下餓漢式的單例。

     

    如上所說,餓漢式單例是指:指全局的單例實例在類裝載時構(gòu)建的實現(xiàn)方式。

     

    由于類裝載的過程是由類加載器(ClassLoader)來執(zhí)行的,這個過程也是由JVM來保證同步的,所以這種方式先天就有一個優(yōu)勢——能夠免疫許多由多線程引起的問題。

     

    4.1 餓漢式單例的實現(xiàn)方式

     

    餓漢式單例的實現(xiàn)如下:

    復(fù)制代碼
    //餓漢式實現(xiàn)  public class SingleB {      private static final SingleB INSTANCE = new SingleB();      private SingleB() {}      public static SingleB getInstance() {          return INSTANCE;      }  }
    復(fù)制代碼

    對于一個餓漢式單例的寫法來說,它基本上是完美的了。

     

    所以它的缺點也就只是餓漢式單例本身的缺點所在了——由于INSTANCE的初始化是在類加載時進(jìn)行的,而類的加載是由ClassLoader來做的,所以開發(fā)者本來對于它初始化的時機(jī)就很難去準(zhǔn)確把握:

     

    1. 可能由于初始化的太早,造成資源的浪費

       

    2. 如果初始化本身依賴于一些其他數(shù)據(jù),那么也就很難保證其他數(shù)據(jù)會在它初始化之前準(zhǔn)備好。

     

    當(dāng)然,如果所需的單例占用的資源很少,并且也不依賴于其他數(shù)據(jù),那么這種實現(xiàn)方式也是很好的。

     

    知識點:什么時候是類裝載時?

     

    前面提到了單例在類裝載時被實例化,那究竟什么時候才是『類裝載時』呢?

     

    不嚴(yán)格的說,大致有這么幾個條件會觸發(fā)一個類被加載:

     

    1. new一個對象時

    2. 使用反射創(chuàng)建它的實例時

    3. 子類被加載時,如果父類還沒被加載,就先加載父類

    4. jvm啟動時執(zhí)行的主類會首先被加載

     

    5. 一些其他的實現(xiàn)方式

     

    5.1 Effective Java 1 —— 靜態(tài)內(nèi)部類

     

    《Effective Java》一書的第一版中推薦了一個中寫法:

    復(fù)制代碼
    // Effective Java 第一版推薦寫法  public class Singleton {      private static class SingletonHolder {          private static final Singleton INSTANCE = new Singleton();      }      private Singleton (){}      public static final Singleton getInstance() {          return SingletonHolder.INSTANCE;      }  }
    復(fù)制代碼

    這種寫法非常巧妙:

     

    • 對于內(nèi)部類SingletonHolder,它是一個餓漢式的單例實現(xiàn),在SingletonHolder初始化的時候會由ClassLoader來保證同步,使INSTANCE是一個真·單例。

       

    • 同時,由于SingletonHolder是一個內(nèi)部類,只在外部類的Singleton的getInstance()中被使用,所以它被加載的時機(jī)也就是在getInstance()方法第一次被調(diào)用的時候。

     

    ——它利用了ClassLoader來保證了同步,同時又能讓開發(fā)者控制類加載的時機(jī)。從內(nèi)部看是一個餓漢式的單例,但是從外部看來,又的確是懶漢式的實現(xiàn)。

     

    簡直是神乎其技。

     

    5.2 Effective Java 2 —— 枚舉

     

    你以為到這就算完了?不,并沒有,因為厲害的大神又發(fā)現(xiàn)了其他的方法。

     

    《Effective Java》的作者在這本書的第二版又推薦了另外一種方法,來直接看代碼:

    復(fù)制代碼
    // Effective Java 第二版推薦寫法  public enum SingleInstance {      INSTANCE;      public void fun1() {           // do something      }  }     // 使用  SingleInstance.INSTANCE.fun1();
    復(fù)制代碼

    看到了么?這是一個枚舉類型……連class都不用了,極簡。

     

    由于創(chuàng)建枚舉實例的過程是線程安全的,所以這種寫法也沒有同步的問題。

     

    作者對這個方法的評價:

     

    這種寫法在功能上與共有域方法相近,但是它更簡潔,無償?shù)靥峁┝诵蛄谢瘷C(jī)制,絕對防止對此實例化,即使是在面對復(fù)雜的序列化或者反射攻擊的時候。雖然這中方法還沒有廣泛采用,但是單元素的枚舉類型已經(jīng)成為實現(xiàn)Singleton的最佳方法。

     

    枚舉單例這種方法問世一些,許多分析文章都稱它是實現(xiàn)單例的最完美方法——寫法超級簡單,而且又能解決大部分的問題。

     

    不過我個人認(rèn)為這種方法雖然很優(yōu)秀,但是它仍然不是完美的——比如,在需要繼承的場景,它就不適用了。

     

    6. 總結(jié)

     

    OK,看到這里,你還會覺得單例模式是最簡單的設(shè)計模式了么?再回頭看一下你之前代碼中的單例實現(xiàn),覺得是無懈可擊的么?

     

    可能我們在實際的開發(fā)中,對單例的實現(xiàn)并沒有那么嚴(yán)格的要求。比如,我如果能保證所有的getInstance都是在一個線程的話,那其實第一種最簡單的教科書方式就夠用了。再比如,有時候,我的單例變成了多例也可能對程序沒什么太大影響……

     

    但是,如果我們能了解更多其中的細(xì)節(jié),那么如果哪天程序出了些問題,我們起碼能多一個排查問題的點。早點解決問題,就能早點回家吃飯……

     

    —— 還有,完美的方案是不存在,任何方式都會有一個『度』的問題。比如,你的覺得代碼已經(jīng)無懈可擊了,但是因為你用的是JAVA語言,可能ClassLoader有些BUG啊……你的代碼誰運(yùn)行在JVM上的,可能JVM本身有BUG啊……你的代碼運(yùn)行在手機(jī)上,可能手機(jī)系統(tǒng)有問題啊……你生活在這個宇宙里,可能宇宙本身有些BUG啊……

     

    所以,盡力做到能做到的最好就行了。

     

    —— 感謝你花費了不少時間看到這里,但愿你沒有覺得虛度。

    posted on 2018-11-27 22:51 fly 閱讀(116) 評論(0)  編輯  收藏 所屬分類: 設(shè)計模式
    主站蜘蛛池模板: 国内一级一级毛片a免费| 亚洲国产成人精品激情| 国产大片51精品免费观看| 久久久精品2019免费观看| sihu国产精品永久免费| 亚洲日韩AV一区二区三区四区| 亚洲精品成人av在线| 久久夜色精品国产亚洲av| 在线日韩av永久免费观看| 国产精品成人观看视频免费| 免费国产成人午夜在线观看| 一级做a爰片久久免费| 国产亚洲精彩视频| 亚洲国产成人AV在线播放| 亚洲人成黄网在线观看 | 国产男女爽爽爽免费视频| 国产精品亚洲专区无码WEB | 国产精品视频免费一区二区| 中文字幕免费视频| 无码免费一区二区三区免费播放| 好猛好深好爽好硬免费视频 | 亚洲日韩精品无码专区网址| 亚洲国产成人久久综合区| 国产小视频免费观看| 国产日产成人免费视频在线观看| 成人毛片免费在线观看| 97无码免费人妻超级碰碰碰碰| 香蕉97超级碰碰碰免费公| 成年黄网站色大免费全看| 免费v片在线观看视频网站| 精品国产sm捆绑最大网免费站 | 亚洲国产精品久久网午夜| 久久精品亚洲中文字幕无码麻豆| 久久精品国产亚洲AV麻豆网站| 亚洲AV无码一区二区乱孑伦AS| 亚洲精品亚洲人成在线观看| 亚洲国产婷婷六月丁香| 亚洲av日韩av激情亚洲| 亚洲欧洲日韩综合| 亚洲黄色免费在线观看| 亚洲一区二区三区91|