1. 程式人生 > >設計模式 之 單列設計模式

設計模式 之 單列設計模式

語句 原子性 了解 right 一個 技術分享 設計模式 美的 fin

本文章是在學習了 微信公眾號 “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/

設計模式 之 單列設計模式