首頁 > 軟體

Java多執行緒案例之單例模式懶漢+餓漢+列舉

2022-06-02 18:01:12

前言:

本篇文章將介紹Java多執行緒中的幾個典型案例之單例模式,所謂單例模式,就是一個類只有一個範例物件,本文將著重介紹在多執行緒的背景下,單例模式的簡單實現。

1.單例模式概述

單例模式,是一種常用的軟體設計模式。在它的核心結構中只包含一個被稱為單例的特殊類。通過單例模式可以保證系統中,應用該模式的類一個類只有一個範例,即一個類只有一個物件範例。

單例模式有兩種典型的實現,一是餓漢模式,二是懶漢模式,餓漢模式中的“餓”並不是真的表示“餓”,更加準確的來說應該是表示“急”,那就是一個單例類還沒被使用,它的單例物件就已經建立好了,而懶漢模式,要等到使用這個單例類時才建立單例物件。

單例模式中的單例類,只能擁有一個範例物件,又static修飾的成員是屬於類的,也就是隻有一份,所以我們可以使用static修飾的成員變數儲存範例物件的參照。

2.單例模式的簡單實現

2.1餓漢模式

由於單例模式中,一個類只能擁有一個範例物件,所以需要將類構造方法封裝,防止類被建立多個範例物件,但是在使用該類時必須要得到該類的範例物件,因此我們得建立一個獲取該唯一範例物件的方法getInstance

而對於該類的範例物件,在類中我們可以使用屬於類的成員變數來儲存(即static成員變數)。

//單例模式 - 餓漢模式
class HungrySingleton {
    //1.使用一個變數來儲存該類唯一的範例,因為單例模式在一個程式中只能擁有一個範例,由於static成員只有一份,我們可以使用static變數來儲存
    private static final HungrySingleton instance = new HungrySingleton();

    //2.封裝構造方法,防止該類被範例出新的物件
    private HungrySingleton() {}

    //3.獲取該類的唯一範例物件
    public HungrySingleton getInstance() {
        return instance;
    }
}

多執行緒情況下,對於上述簡單實現的餓漢式單例模式,只需要考慮getInstance方法是否執行緒安全即可,由於該方法就一句返回語句,即一次讀操作,而讀操作是執行緒安全的,所以getInstance方法也就是執行緒安全的,綜上餓漢式單例模式是執行緒安全的。

2.2懶漢模式

懶漢模式相比於餓漢模式,區別就是範例物件建立時機不同,懶漢模式需要等到第一次使用時才建立範例物件,所以僅僅只需要修改獲取物件的方法即可。

不考慮多執行緒情況,懶漢模式實現程式碼如下:

//單例模式 - 懶漢模式
class SlackerSingleton {
    //1.使用一個變數來儲存該類唯一的範例,因為單例模式在一個程式中只能擁有一個範例,由於static成員只有一份,我們可以使用static變數來儲存
    //懶漢單例模式是在使用的時候建立物件,因此初始時物件不應該被建立
    private static SlackerSingleton instance;

    //2.封裝構造方法,防止該類被範例出新的物件
    private SlackerSingleton() {}

    //3.獲取該類的唯一物件,如果沒有就建立
    public SlackerSingleton getInstance() {
        if (instance == null) {
            instance = new SlackerSingleton();
        }
        return instance;
    }
}

多執行緒情況下,由於getInstance方法中存在兩次讀(一次判斷一次返回)操作一次寫操作(修改intsance變數的值),instance變數為初始化時(即instance=null)可能會存在多個執行緒進入判斷語句,這樣該類可能會被範例出多個物件,所以上述實現的懶漢式單例模式是執行緒不安全的。

造成執行緒不安全的程式碼段為if語句裡面的讀操作和instance的修改操作,所以我們需要對這段程式碼進行加鎖,然後就得到了執行緒安全的懶漢模式:

//多執行緒情況下餓漢模式獲取物件時唯讀不修改,所以是執行緒安全的
//多執行緒情況下懶漢模式獲取物件時存在兩次讀操作,分別為判斷instance是否為null和返回instance,除了讀操作還存在修改操作,即新建物件並使instance指向該物件
//懶漢模式物件還未初始化的時候,可能會存在多個執行緒進入判斷語句,會導致範例出多個物件,因此懶漢單例模式是執行緒不安全的。

//執行緒安全單例模式 - 懶漢模式
class SafeSlackerSingleton {
    //1.使用一個變數來儲存該類唯一的範例,因為單例模式在一個程式中只能擁有一個範例,由於static成員只有一份,我們可以使用static變數來儲存
    //懶漢單例模式是在使用的時候建立物件,因此初始時物件不應該被建立
    private static SafeSlackerSingleton instance;

    //2.封裝構造方法,防止該類被範例出新的物件
    private SafeSlackerSingleton() {}

    //3.獲取該類的唯一物件,如果沒有就建立
    public SafeSlackerSingleton getInstance() {
        synchronized (SafeSlackerSingleton.class) {
            if (instance == null) {
                instance = new SafeSlackerSingleton();
            }
        }
        return instance;
    }
}

但是!上述執行緒安全問題只出現在instance沒有初始化的時候,如果instance已經初始化了,那個判斷語句就是個擺設,就和餓漢模式一樣,就是執行緒安全的了,如果按照上面的程式碼處理執行緒安全問題,不論instance是否已經初始化,都要進行加鎖,因此會使鎖競爭加劇,消耗沒有必要消耗的資源,所以在加鎖前需要先判斷一下instance是否已經初始化,如果為初始化就進行加鎖。

按照上述方案得到以下關於獲取物件的方法程式碼:

    public SafeSlackerSingletonPlus getInstance() {
        //判斷instance是否初始化,如果已經初始化了,那麼該方法只有兩個讀操作,本身就是執行緒安全的,不需要加鎖了,這樣能減少鎖競爭,提高效率
        if (instance == null) {
            synchronized (SafeSlackerSingletonPlus.class) {
                if (instance == null) {
                    instance = new SafeSlackerSingletonPlus();
                }
            }
        }
        return instance;
    }

到這裡執行緒安全的問題是解決了,但是別忘了編譯器它是不信任你的,它會對你寫的程式碼進行優化! 上面所寫的程式碼需要判斷instance==null,而多執行緒情況下,很可能頻繁進行判斷,這時候執行緒不會去讀記憶體中的資料,而會直接去暫存器讀資料,這時候instance值變化時,執行緒完全感知不到!造成記憶體可見性問題,為了解決該問題需要使用關鍵字volatile修飾instance變數,防止編譯器優化,從而保證記憶體可見性。

//執行緒安全優化單例模式 - 懶漢模式
class SafeSlackerSingletonPlus {
    //1.使用一個變數來儲存該類唯一的範例,因為單例模式在一個程式中只能擁有一個範例,由於static成員只有一份,我們可以使用static變數來儲存
    //懶漢單例模式是在使用的時候建立物件,因此初始時物件不應該被建立
    private static volatile SafeSlackerSingletonPlus instance;

    //2.封裝構造方法,防止該類被範例出新的物件
    private SafeSlackerSingletonPlus() {}

    //3.獲取該類的唯一物件,如果沒有就建立
    public SafeSlackerSingletonPlus getInstance() {
        //判斷instance是否初始化,如果已經初始化了,那麼該方法只有兩個讀操作,本身就是執行緒安全的,不需要加鎖了,這樣能減少鎖競爭,提高效率
        //如果執行緒很多,頻繁進行外層或內層if判斷,可能會引發內層可見性問題,因此要給instan變數加上volatile
        if (instance == null) {
            synchronized (SafeSlackerSingletonPlus.class) {
                if (instance == null) {
                    instance = new SafeSlackerSingletonPlus();
                }
            }
        }
        return instance;
    }
}

2.3列舉實現單例模式

除了使用餓漢和懶漢模式還可以使用列舉的方式實現,在《Effective Java》書中有這樣一句話:單元素的列舉型別已經成為實現Singleton的最佳方法。 因為列舉就是一個天然的單例,並且列舉型別通過反射都無法獲取封裝的私有變數,非常安全。

//單元素的列舉型別已經成為實現Singleton的最佳方法
enum  EnumSingleton {
    INSTANCE;
    public void doSomething() {
        System.out.println("完成一些任務!");
    }
}

使用方式:

public class Singleton {
    public static void main(String[] args) {
        EnumSingleton.INSTANCE.doSomething();
    }
}

執行結果:

好了,有關多執行緒單例模式問題就討論到這裡了,你學會了嗎?

到此這篇關於Java多執行緒案例之單例模式懶漢+餓漢+列舉的文章就介紹到這了,更多相關Java單例模式內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com