設計模式之單例模式|8月更文挑戰

image.png

今天咱們正式進入java設計模式的學習之旅,先從單例模式開始講起。java

你們能夠查看我設計模式專欄篇的引文,關注個人設計模式專欄:java設計模式攻堅準備程序員

話很少說,進入正題算法

單例模式

  • 單例設計模式(Singleton Design Pattern)理解起來很是簡單。一個類只容許建立一個對象(或者實例),那這個類就是一個單例類,這種設計模式就叫做單例設計模式,簡稱單例模式。數據庫

  • 咱們在編程開發中常常會遇到這種場景:須要保證一個類只有一個實例,哪怕多線程同時訪問,並且須要提供一個全局訪問此實例的點。能夠總結出一個理論,單例模式主要解決的是一個全局使用的類,被頻繁地建立與銷燬,從而提高代碼的總體性能編程

  • 應該看到這裏大概都有所瞭解了,晦澀的文字對於單例模式的描述也比較容易理解了。那麼咱們看下經典的單例場景:設計模式

  • 如數據庫的鏈接池不會反覆建立,Spring中一個單例模式Bean的生成和使用,代碼中須要設置全局的一些屬性並保存。安全

這就是單例模式,那麼針對上述描述咱們下面咱們用實際案例和代碼來讓你有更深刻的理解。markdown

7+2種單例模式實現方式

單例模式的實現方式比較多,可是惟一隻有一個目的,永遠只建立一個實例(對象),下面咱們先對7種常見的案例一一進行闡述,多線程

爲何還有2種呢,對的 由於咱們只有一個目的,保證只建立一個實例,那麼任何方式只要知足這個均可以,我會寫出兩種不怎麼常見的,可是我的認爲可使用的方式來設計單例模式。併發

一、靜態類

/** * 單例靜態類 * @Date 2021/8/1 5:43 下午 * @Author yn */
public class Singleton_01 {
    public static Map<String,Object> cacheInfo = new ConcurrentHashMap<>();
}


public static void main(String[] args) {
    Map<String,Object> map =  Singleton_01.cacheInfo;
}
複製代碼

這種靜態類方式在平常的業務開發中很常見,它能夠在第一次運行時直接初始化Map類,用於全局訪問,使用靜態類方式更加方便

二、懶漢模式(線程不安全)

/** * 單例之懶漢模式 * @Date 2021/8/1 5:43 下午 * @Author yn */
public class Singleton_02 {

    private static Singleton_02 instance;

    private Singleton_02(){

    }
    //獲取對象實例
    public static Singleton_02 getInstance(){
        if (null != instance){
            return instance;
        }
        //若是爲空 則內部new對象
        instance = new Singleton_02();
        return instance;
    }
}

//獲取對象
public static void main(String[] args) {
    Singleton_02 singleton_02 =  Singleton_02.getInstance();
}

複製代碼

單例模式有一個特別重要的特色是不容許外部直接建立,也就是 new Singleton_02(),所以這裏在默認的構造函數上添加了私有屬性private。

思考:若是有多個訪問者同時獲取對象實例,會不會形成多個一樣的實例並存。答案是確定的。 若是當第一次訪問尚未建立完成實例(正在建立中),結果第二個線程請求進來了,都會走到new實例當中

三、懶漢模式(線程安全)

咱們在上述的基礎之上作下處理:添加 synchronized 鎖控制

/** * 單例之懶漢模式 synchronized加持 * @Date 2021/8/1 5:43 下午 * @Author yn */
public class Singleton_01 {

    private static Singleton_01 instance;

    private Singleton_01(){
    }

    //獲取對象實例加 synchronized 控制,
    public static synchronized Singleton_01 getInstance(){
        if (null != instance){
            return instance;
        }
        //若是爲空 則內部new對象
        instance = new Singleton_01();
        return instance;
    }
}
複製代碼

synchronized加鎖,便保證了每次只能有一個線程加鎖成功,那便只能new一次對象。

此種模式雖然解決了線程不安全的問題,但因爲把鎖加到方法中後,全部的訪問由於須要鎖佔用,致使資源浪費。除非在特殊狀況下,不然不建議用此種方式實現單例模式。

四、餓漢模式(線程安全)

/** * 餓漢模式 線程安全 * @Date 2021/8/1 5:43 下午 * @Author yn */
public class Singleton_03 {

    private static Singleton_03 instance = new Singleton_03();

    private Singleton_03(){
    }

    //獲取對象實例
    public static Singleton_03 getInstance(){
        return instance;
    }
}
複製代碼

這種方式與開頭的第一個實例化 Map 基本一致,在程序啓動時直接運行加載,後續有外部須要使用時獲取便可。這種方式並非懶加載,也就是說不管程序中是否用到這樣的類,都會在程序啓動之初進行建立。

五、類的內部類(線程安全)

/** * 單例模式 匿名內部類 * @Date 2021/8/1 5:43 下午 * @Author yn */
public class Singleton_04 {

    private static class singletonHolder {
        private static Singleton_04 instance = new Singleton_04();
    }

    private Singleton_04(){
    }

    //獲取對象實例
    public static Singleton_04 getInstance(){
        return singletonHolder.instance;
    }
}
複製代碼

使用類的靜態內部類實現的單例模式,既保證了線程安全,又保證了懶漢模式,同時不會由於加鎖而下降性能。

這主要是由於JVM虛擬機能夠保證多線程併發訪問的正確性,也就是一個類的構造方法在多線程環境下能夠被正確地加載。這也是推薦使用的一種單例模式。

虛擬機會保證一個類的類構造器在多線程環境中被正確的加鎖、同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的類構造器,其餘線程都須要阻塞等待,直到活動線程執行方法完畢。

因此線程安全!!!

六、雙重鎖校驗(線程安全)

/** * 雙重鎖校驗(線程安全) * @Date 2021/8/1 5:43 下午 * @Author yn */
public class Singleton_05 {

    private static volatile Singleton_05 instance;

    private Singleton_05(){

    }

    //獲取對象實例
    public static Singleton_05 getInstance(){
        if (null != instance){
            return instance;
        }
        synchronized (Singleton_05.class){
            if (null == instance){
                instance = new Singleton_05();
            }
        }
        return instance;
    }
}
複製代碼

這種方式是極爲彌補懶漢模式的不足,無需每次都去上鎖。雙重鎖的方式是方法級鎖的優化,減小了獲取實例的耗時。能夠適當運用此模式,

七、枚舉單例(線程安全)

/** * 單例模式之枚舉 * @Date 2021/8/1 8:04 下午 * @Author yn */
public enum Singleton_06 {

    INSTANCE;

    public void testInstace(){
        System.out.println("我是單例模式之枚舉 --> -->");
    }
}
複製代碼

此種方式多是平時最少用到的。可是這種方式解決了最主要的線程安全、自由串行化和單一實例問題。調用方式以下:

public static void main(String[] args) {
    //調用輸出
    Singleton_06.INSTANCE.testInstace();
}
複製代碼

相比之下,你就會發現,枚舉實現單例的代碼會精簡不少。

這種寫法雖然在功能上與共有域的方法接近,可是它更簡潔。即便在面對複雜的串行化或反射攻擊時,也無償地提供了串行化機制,絕對防止對此實例化。單元素的枚舉類型已經成爲實現Singleton的最佳方法(我的認爲),值得推薦運用!

其實,並非使用枚舉就不須要保證線程安全,只不過線程安全的保證不須要咱們關心而已。也就是說,其實在「底層」仍是作了線程安全方面的保證的。這就涉及到jvm的問題,能夠經過反編譯enum類可瞭解到底層的東西,今天在這裏不贅述。

OK,到這裏,經典的七種建立單例模式的方式已經講完,接下來再說兩種方法來達到單例的目的。

利用 CAS「AtomicReference」 實現單例(線程安全)

/** * 利用cas思想實現只有一個實例 * @Date 2021/8/1 5:43 下午 * @Author yn */
public class Singleton_07 {

    //利用cas思想管理線程安全
    private static final AtomicReference<Singleton_07> INSTANCE = new AtomicReference<Singleton_07>();

    private Singleton_07(){

    }

    //獲取對象實例
    public static final Singleton_07 getInstance(){

        for (;;){
            //經過 AtomicReference get方法實現線程安全
            //cas思想 每次都去查詢是否存在當前對象
            Singleton_07 instance = INSTANCE.get();
            if (null != instance) return instance;
            INSTANCE.compareAndSet(null,new Singleton_01());
            return INSTANCE.get();
        }
    }

    public static void main(String[] args) {
        //一次請求
        System.out.println(Singleton_07.getInstance());
        //二次請求
        System.out.println(Singleton_07.getInstance());
        
        //類實例同一個
    }
}
複製代碼

Java 併發庫提供了不少原子類支持併發訪問的數據安全性,如:AtomicInteger、AtomicBoolean、AtomicLong 和 AtomicReference。AtomicReference 能夠封裝引用一個V實例,

上面支持併發訪問的單例模式就是利用了這種特性。使用CAS的好處是不須要使用傳統的加鎖方式,而是依賴CAS的忙等算法、底層硬件的實現保證線程安全。相對於其餘鎖的實現,沒有線程的切換和阻塞也就沒有了額外的開銷,而且能夠支持較大的併發。固然,CAS也有一個缺點就是忙等,若是一直沒有獲取到,會陷於死循環。

利用 ThreadLocal實現單例

若是你們對ThreadLocal有不明白的地方,能夠看個人歷史文章: 乾貨!ThreadLocal 使用場景

/** * 利用ThreadLocal 實現單例,只保存一個對象實例 * @Date 2021/8/1 8:30 下午 * @Author yn */
public class AppContext {
    private static final ThreadLocal<AppContext> local = new ThreadLocal<>();
    private Map<String,Object> data = new HashMap<>();
    public Map<String, Object> getData() {
        return getAppContext().data;
    }
    //批量存數據
    public void setData(Map<String, Object> data) {
        getAppContext().data.putAll(data);
    }
    //存數據
    public void set(String key, String value) {
        getAppContext().data.put(key,value);
    }
    //取數據
    public void get(String key) {
        getAppContext().data.get(key);
    }
    //初始化的實現方法
    private static AppContext init(){
        AppContext context = new AppContext();
        local.set(context);
        return context;
    }
    //作延遲初始化
    public static AppContext getAppContext(){
        AppContext context = local.get();
        if (null == context) {
            context = init();
        }
        return context;
    }
    //刪除實例
    public static void remove() {
        local.remove();
    }
}
複製代碼

上面的代碼實現實際上就是懶漢式初始化的擴展,只不過用 ThreadLocal 替換靜態對象來存儲惟一對象實例。之所會選擇 ThreadLocal,就是由於 ThreadLocal 相比傳統的線程同步機制更有優點。

在傳統的同步機制中,咱們一般會經過對象的鎖機制來保證同一時間只有一個線程訪問單例類。這時該類是多個線程共享的,咱們都知道使用同步機制時,何時對類進行讀寫、何時鎖定和釋放對象是有很煩瑣要求的,這對於通常的程序員來講,設計和編寫難度相對較大

而 ThreadLocal 則會爲每個線程提供一個獨立的對象副本,從而解決了多個線程對數據的訪問衝突的問題。正由於每個線程都擁有本身的對象副本,也就省去了線程之間的同步操做

因此說,如今絕大多數單例模式的實現基本上都是採用的 ThreadLocal 這一種實現方式。

總結

雖然單例模式只是一個很日常的模式,但在各類的實現上卻須要用到Java的基本功,包括懶漢模式、餓漢模式、線程是否安全、靜態類、內部類、加鎖和串行化等。在平常開發中,咱們要根據實際狀況去選擇其中的一種方式。

感謝您的閱讀,創做不易,歡迎點贊,關注, 轉發,感謝,你們能夠點擊我頭像查看歷史乾貨文章。

設計模式本月我持續更新,歡迎關注下方的設計模式專欄,我會持續更新 咱們下期再見!

相關文章
相關標籤/搜索