設計模式 之 單列設計模式

本文章是在學習了 微信公衆號   「java後端技術 」 以後本身的學習筆記  。 其中直接 複製了 至關部分的原做者的原文。java

    若是您看到了個人這篇文章, 推薦您 查看原文後端

原文鏈接  :   https://mp.weixin.qq.com/s/CfekzTTT-a066_PyT_n_eA   設計模式

 

在之前本身也瞭解過一些設計模式, 這其中就包括了單例模式, 可是 對單例模式只限於 基本的     懶漢式    和    餓漢式 :安全

 餓漢式代碼示例 :微信

public DemoSingle{
    //私有化構造器
    private DemoSingle(){}    
    //提早構造好方法
    private static DemoSingle single = new DemoSingle();
    //提供暴露對象的方法
     public DemoSingle getDemoSingle(){
             return  single;
      }
}        

懶漢式代碼示例 :多線程

/**
 * Create by yaoming  on  2018/4/27
 */
public class DemoSingle {
    //私有化構造方法
    private DemoSingle(){}
    //私有化 本類對象引用
    private static DemoSingle single = null;
    //獲得本類方法的引用
    public DemoSingle getDemoSingel(){
        synchronized (DemoSingle.class){
            if(single == null){
                single = new DemoSingle();
            }
        }
        return single;
    }
}

  

所謂單列模式就是說, 全局在任何一個地方發使用到的該類對象都是同一個對象,首先要保證 對象一直存在(一直有引用指向對象),因此,使用一個靜態引用併發

指向該類。 同時要保證 只有一個對象, 因此要私有化 構造方法, 使得只有本身能構造這個對象(並且本身必須構造且之構造一個該對象)。函數

餓漢式    是在加載該類的時候就進行了對象的創建,不管咱們是否使用到了 這個對象。 其安全有效, 不涉及多線程操做。 可是其形成了資源的浪費。性能

懶漢式    在實際狀況中咱們可能爲了性能着想, 每每但願能使用延遲加載的方式來建立對象, 這個就是懶漢式了。學習

    上面的懶漢式代碼,爲了考慮多線程的關係, 加了一個同步代碼塊, 這樣雖然解決了 多線程安全問題, 可是卻由於每次都會進行一個同步狀況下的判斷,

    每每使得效率並,並無增長,  用原文做者的話來講就是 : 使用一個 百分之百的盾 來 阻擋一個 百分之一 的出現的問題。 這顯然不合適。

遂優化 :

public class DemoSingle {
    //私有化構造方法
    private DemoSingle(){}
    //私有化 本類對象引用
    private static DemoSingle single = null;
    //獲得本類方法的引用
    public DemoSingle getDemoSingel(){
        if(single == null){
            synchronized (DemoSingle.class){
                if(single == null){
                    single = new DemoSingle();
                }
            }
        }
        return single;
    }
}

 

這個代碼就是原來我對於懶漢式的理解了, 在看了原做者的文章後, 才發如今這個看似完美的代碼下面隱藏的問題,

這裏 原做者 談到了兩個概念 : 原子操做 和 指令重排 

這裏是做者原文 :

  

原子操做:
簡單來講,原子操做(atomic)就是不可分割的操做,在計算機中,就是指不會由於線程調度被打斷的操做。好比,簡單的賦值是一個原子操做:

 

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

假如m原先的值爲0,那麼對於這個操做,要麼執行成功m變成了6,要麼是沒執行 m仍是0,而不會出現諸如m=3這種中間態——即便是在併發的線程中。

可是,聲明並賦值就不是一個原子操做:

 

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

對於這個語句,至少有兩個操做:①聲明一個變量n ②給n賦值爲6——這樣就會有一箇中間狀態:變量n已經被聲明瞭可是尚未被賦值的狀態。這樣,在多線程中,因爲線程執行順序的不肯定性,若是兩個線程都使用m,就可能會致使不穩定的結果出現。

指令重排:
簡單來講,就是計算機爲了提升執行效率,會作的一些優化,在不影響最終結果的狀況下,可能會對一些語句的執行順序進行調整。好比,這一段代碼:

 

int a ;   // 語句1
a = 8 ;   // 語句2
int b = 9 ;     // 語句3
int c = a + b ; // 語句4

正常來講,對於順序結構,執行的順序是自上到下,也即1234。可是,因爲指令重排
的緣由,由於不影響最終的結果,因此,實際執行的順序可能會變成3124或者1324。

因爲語句3和4沒有原子性的問題,語句3和語句4也可能會拆分紅原子操做,再重排。——也就是說,對於非原子性的操做,在不影響最終結果的狀況下,其拆分紅的原子操做可能會被從新排列執行順序。

OK,瞭解了原子操做和指令重排的概念以後,咱們再繼續看代碼三的問題。

主要在於singleton = new Singleton()這句,這並不是是一個原子操做,事實上在 JVM 中這句話大概作了下面 3 件事情。
  1. 給 singleton 分配內存
  2. 調用 Singleton 的構造函數來初始化成員變量,造成實例
  3. 將singleton對象指向分配的內存空間(執行完這步 singleton纔是非 null了)

在JVM的即時編譯器中存在指令重排序的優化。
  
也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序多是 1-2-3 也多是 1-3-2。若是是後者,則在 3 執行完畢、2 未執行以前,被線程二搶佔了,這時 instance 已是非 null 了(但卻沒有初始化),因此線程二會直接返回 instance,而後使用,而後瓜熟蒂落地報錯。
  
再稍微解釋一下,就是說,因爲有一個『instance已經不爲null可是仍沒有完成初始化』的中間狀態,而這個時候,若是有其餘線程恰好運行到第一層if (instance ==null)這裏,這裏讀取到的instance已經不爲null了,因此就直接把這個中間狀態的instance拿去用了,就會產生問題。這裏的關鍵在於線程T1對instance的寫操做沒有完成,線程T2就執行了讀操做。

 

因而可知, 個人第二段 懶漢式代碼存在 隱患 , 根據做者思路 將之改成 :

public class DemoSingle {
    //私有化構造方法
    private DemoSingle(){}
    //私有化 本類對象引用
    private static volatile DemoSingle single = null;
    //獲得本類方法的引用
    public DemoSingle getDemoSingel(){
        if(single == null){
            synchronized (DemoSingle.class){
                if(single == null){
                    single = new DemoSingle();
                }
            }
        }
        return single;
    }
}

  其實就是加上了一個 volatitle 關鍵字 ,   這裏  volatitle  關鍵字的做用是禁止 指令重排, 在對 single 進行復制完成以前是不會進行 讀操做的。

       (做者原文 注意:volatile阻止的不是singleton = new Singleton()這句話內部[1-2-3]的指令重排,而是保證了在一個寫操做([1-2-3])完成以前,不會調用讀操做(if (instance == null))。)

 

  

  這樣就解決了傳統的 懶漢式單例模式 的多線程安全問題, 除此以外 原做者還提供了 其餘兩種更爲簡便的 方式:

  

靜態內部類:

public class DemoSingle {
    //私有化構造方法
    private DemoSingle(){}
    //靜態內部類
    private static class DemoSingleHand{
        private static final DemoSingle DEMO_SINGLE = new DemoSingle();
    }
    //得到該類對象的方法
    public static DemoSingle getDemoSingel(){
        return DemoSingleHand.DEMO_SINGLE;
    }
}

  

這種寫法的巧妙之處在於:對於內部類SingletonHolder,它是一個餓漢式的單例實現,在SingletonHolder初始化的時候會由ClassLoader來保證同步,使INSTANCE是一個真單例。

同時,因爲SingletonHolder是一個內部類,只在外部類的Singleton的getInstance()中被使用,因此它被加載的時機也就是在getInstance()方法第一次被調用的時候。
  
它利用了ClassLoader來保證了同步,同時又能讓開發者控制類加載的時機。從內部看是一個餓漢式的單例,可是從外部看來,又的確是懶漢式的實現

枚舉:

是否是很簡單?並且由於自動序列化機制,保證了線程的絕對安全。三個詞歸納該方式:簡單、高效、安全

這種寫法在功能上與共有域方法相近,可是它更簡潔,無償地提供了序列化機制,絕對防止對此實例化,即便是在面對複雜的序列化或者反射攻擊的時候。雖然這中方法尚未普遍採用,可是單元素的枚舉類型已經成爲實現Singleton的最佳方法。

 

原文地址:https://gyl-coder.top/Java%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/

相關文章
相關標籤/搜索