01.Singleton Pattern 單例模式

来源:https://www.cnblogs.com/Acechengui/archive/2022/09/11/16684708.html
-Advertisement-
Play Games

Singleton Pattern 單例模式,作為創建型模式的一種,其保證了類的實例對象只有一個,並對外提供此唯一實例的訪問介面 概述 對於單例模式而言,其最核心的目的就是為了保證該類的實例對象是唯一的。為此一方面,需要將該類的構造函數設為private,另一方面,該類需要在內部完成實例的構造並對外 ...


Singleton Pattern 單例模式,作為創建型模式的一種,其保證了類的實例對象只有一個,並對外提供此唯一實例的訪問介面

在這裡插入圖片描述

概述

對於單例模式而言,其最核心的目的就是為了保證該類的實例對象是唯一的。為此一方面,需要將該類的構造函數設為private,另一方面,該類需要在內部完成實例的構造並對外提供訪問介面。單例模式的好處顯而易見,可以避免頻繁創建、銷毀實例所帶來的性能開銷;但其缺點也同樣明顯,此類不僅需要描述業務邏輯,同時還需要構造出該類的唯一對象並對外提供訪問介面,其顯然違背了單一職責原則

實現

單例模式的思想雖然簡單易懂,但實現起來卻可謂是花樣繁多、妙不可言。這裡來介紹幾種常見的單例模式的實現

餓漢式

如下實現最為簡單,當 SingletonDemo1 類被載入到JVM中,即會完成實例化。即不是所謂的Lazy Load 延遲載入,故通常被稱之為 “餓漢式” 單例。其最大的問題就在,可能構造出來的實例對象從頭到尾沒有被使用過(沒有調用過getInstance方法),從而浪費記憶體。可能有人會對此有些困惑,SingletonDemo1 類被載入到JVM中了,那肯定是因為調用了getInstance方法啊。難道還有別的原因?答案是肯定的

這裡,我們先簡要補充一些類載入機制的相關知識點。我們知道Java中的類被載入到JVM中,通常會有如下幾個階段:載入、 驗證、準備、解析、初始化等。其中對於初始化階段而言,虛擬機規範嚴格規定了有且僅有以下5種情況必須立即對類進行初始化(而載入、 驗證、準備顯然必須在此之前開始):

  1. 遇到new、getstatic、putstatic或invokestatic類型的位元組碼指令時,在Java代碼層面上就是new對象、讀取或設置類的靜態變數(被final修飾、已在編譯期將結果放入常量池的靜態變數除外)、調用類的靜態方法
  2. 對該類使用反射
  3. 當初始化一個類的時候,如果發現其父類還未初始化,則需要先初始化父類
  4. 當JVM啟動時,虛擬機會先初始化開發者所指定的主類(即main方法所在類)
  5. 當使用JDK 1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後解析的結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且該方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化

說到這裡,大家可能就明白了,如果SingletonDemo1類中還有其他靜態方法,一旦被調用就會導致SingletonDemo1類被載入、初始化,此時即完成了實例的構造。眾所周知,JVM保證了類載入過程的線程安全,所以餓漢式單例同樣是線程安全的

/**
 * 單例模式1,餓漢式
 */
public class SingletonDemo1 {
    private static SingletonDemo1 instance = new SingletonDemo1("我是餓漢式的單例");

    private String description;

    /**
     * 私有構造器
     * @param description
     */
    private SingletonDemo1(String description) {
        this.description = description;
    }

    public void getInfo() {
        System.out.println(description);
    }

    /**
     * 提供實例的訪問介面
     * @return
     */
    public static SingletonDemo1 getInstance() {
        return instance;
    }

    public static void main(String[] args) {
        SingletonDemo1 singletonDemo1 = SingletonDemo1.getInstance();
        singletonDemo1.getInfo();
    }
}

測試結果如下所示

懶漢式

前面說到,餓漢式單例會導致記憶體空間的浪費,那麼有沒有辦法解決這個問題呢?答案是有的,這就是”懶漢式”單例。顧名思義,其實例不是在類載入、初始化時被構建的,而是在真正需要的時候才去創建,如下所示

/**
 * 單例模式2,線程不安全的懶漢式
 */
public class SingletonDemo2 {
    private static SingletonDemo2 instance = null;
    private String description;

    private SingletonDemo2(String description) {
        this.description = description;
    }

    public void getInfo() {
        System.out.println(description);
    }

    public static SingletonDemo2 getInstance() {
        if( instance==null ) {
            instance = new SingletonDemo2("我是線程不安全的懶漢式單例");
        }
        return instance;
    }

    public static void main(String[] args) {
        SingletonDemo2 singletonDemo2 = SingletonDemo2.getInstance();
        singletonDemo2.getInfo();
    }

}

測試結果如下所示

在這裡插入圖片描述

“懶漢式”單例雖然實現了Lazy Load延遲載入,但是其存在一個很嚴重的問題,不是線程安全的。所以如果在多線程環境下,我們需要使用下麵線程安全的”懶漢式”單例,其保障線程安全的手段也很簡單,直接使用synchronized來修飾getInstance方法。這種辦法過於簡單粗暴,同時會導致效率十分低下。實例一旦被構造完畢後,由於鎖的存在,導致每次只能由一個線程可以獲取到實例對象

/**
 * 單例模式3, 線程安全但效率低下的懶漢式
 */
public class SingletonDemo3 {
    private static SingletonDemo3 intance = null;
    private String description;

    private SingletonDemo3(String description) {
        this.description = description;
    }

    public void getInfo() {
        System.out.printf(description);
    }

    public static synchronized SingletonDemo3 getInstance() {
        if( intance==null ) {
            intance = new SingletonDemo3("我是線程安全線程安全但效率低下的懶漢式單例");
        }
        return intance;
    }

    public static void main(String[] args) {
        SingletonDemo3 singletonDemo3 = SingletonDemo3.getInstance();
        singletonDemo3.getInfo();
    }
}

測試結果如下所示
在這裡插入圖片描述

基於DCL(Double-Checked Locking)雙重檢查鎖的單例

通過前面我們看到,無論是餓漢式單例還是懶漢式單例,其都有明顯的缺點。那麼有沒有一種完美的單例?既可以實現Lazy Load延遲載入,又可以在保證線程安全的前提下依然具備較高的效率呢。答案是肯定——基於DCL(Double-Checked Locking)雙重檢查鎖的單例。其實現如下,該單例實現中進行了兩次檢查。第一次檢查時如果發現實例已經構造完畢了,則無需加鎖直接返回實例對象即可。其保證了實例在構建完成後,其他多個線程可以同時快速獲取該實例。第二次檢查時則是為了避免重覆構造實例,因為在還未構造實例前,可能會有多個線程通過了第一次檢查,準備加鎖來構造實例。在DCL的單例實現中,尤其需要註意的一點是靜態變數instance必須要使用volatile進行修飾。其原因在於volatile禁止了指令的重排序。這裡就此問題再作一些詳細的解釋說明:在JDK1.5之前的Java記憶體模型中,雖然不允許volatile變數之間進行重排序,但卻允許普通變數與volatile變數之間的重排序。所以在JSR 133(JDK 1.5)中對volatile變數的記憶體語義進一步增強,即限制了普通變數與volatile變數之間是否可以重排序的具體場景。這也是為什麼在JDK 1.5之前無法通過DCL實現一個線程安全的單例模式

/**
 * 單例模式4,基於DCL的線程安全的單例
 */
public class SingletonDemo4 {
    // 此處必須要使用volatile修飾!
    private static volatile SingletonDemo4 instance = null;
    private String description;

    private SingletonDemo4(String description) {
        this.description = description;
    }

    public void getInfo() {
        System.out.println(description);
    }

    public static SingletonDemo4 getInstance() {
        if( instance==null ) {  // 第一次檢查:如果實例已經構造完成則直接取,避免每次取之前需要獲取鎖
               synchronized (SingletonDemo4.class) {
                    if(instance==null) {    // 第二次檢查:避免構造出多個實例
                        instance = new SingletonDemo4("我是基於DCL的線程安全的單例");
                    }
               }
        }
        return instance;
    }

    public static void main(String[] args) {
        SingletonDemo4 singletonDemo4 = SingletonDemo4.getInstance();
        singletonDemo4.getInfo();
    }

}

測試結果如下
在這裡插入圖片描述

基於靜態內部類的單例

前面我們說到的第一種單例實現,之所以被稱為餓漢式、非延遲載入。其原因就在於類的載入、初始化不能100%保證是因為調用getInstance方法引起的。而這裡我們通過靜態內部類的方式來實現一個延遲載入的單例,代碼如下所示。當調用外部類SingletonDemo5的一些靜態方法(當然getInstance方法除外),只會載入、初始化外部類SingletonDemo5,而不會去初始化靜態內部類SingletonDemo5Holder。只有通過調用getInstance方法訪問了靜態內部類SingletonDemo5Holder的靜態變數instance,靜態內部類SingletonDemo5Holder才會被載入、初始化,顯然此時實例才會被真正的構造。所以對於基於靜態內部類的單例實現而言,其之所以能保證Lazy Load延遲載入特性,是其因為通過SingletonDemo5Holder靜態內部類100%保證了靜態內部類被載入、初始化是因為調用外部類的getInstance方法而導致的。同樣地,該方式的單例也是滿足線程安全的,原因在餓漢式單例實現中已作解釋,此處就不再贅述

/**
 * 單例模式5,靜態內部類
 */
public class SingletonDemo5 {
    private String description;

    private SingletonDemo5(String description) {
        this.description = description;
    }

    public void getInfo() {
        System.out.println(description);
    }

    private static class SingletonDemo5Holder{
        private static final SingletonDemo5 instance = new SingletonDemo5("我是基於靜態內部類的線程安全的單例");
    }

    public static SingletonDemo5 getInstance() {
        return SingletonDemo5Holder.instance;
    }

    public static void main(String[] args) {
        SingletonDemo5 singletonDemo5 = SingletonDemo5.getInstance();
        singletonDemo5.getInfo();
    }
}

測試結果如下所示
在這裡插入圖片描述

基於枚舉的單例

對於Java的枚舉類型而言,其構造器是且只能是private私有的。故其特別適合用於實現單例模式。下麵即是一個基於枚舉的單例實現,可以看到此種實現非常簡潔優雅。當枚舉類進行載入、初始化時,即會完成實例的構建,我們通過枚舉的特性保證了實例的唯一性,當然其不是Lazy Load延遲載入的。與此同時根據類的載入機制我們可知其也是線程安全的(由JVM保證)

/**
 * 單例模式6,枚舉法
 */
public enum SingletonDemo6 {
    INSTANCE("我是枚舉法的單例");

    private String description;

    /**
     * 枚舉的構造器預設訪問許可權是private, 當然也只能是私有的
     * @param description
     */
    SingletonDemo6(String description) {
        this.description = description;
    }

    public void getInfo() {
        System.out.println(description);
    }
}
...
/**
 * 測試用例
 */
public class SingletonDemo6Test {
    public static void main(String[] args) {
        SingletonDemo6 singletonDemo6 = SingletonDemo6.INSTANCE;
        singletonDemo6.getInfo();
    }
}

測試結果如下

在這裡插入圖片描述

參考文獻

  1. Head First 設計模式 弗里曼著
辰鬼丫
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • MySQL架構: 採用C/S架構,即客戶端/伺服器。客戶端和伺服器區分開,通過客戶端發送請求來和伺服器交互。 過程: 用戶通過開發的應用程式來訪問資料庫(C/S),應用程式通過連接器(connecter)連接到資料庫。 連接器包含了各種開發語言的介面,連接完成後MySQL會分配一個線程提供服務,執行 ...
  • MySQL異常sql_mode=only_full_group_by 原因:在MySQL 5.7後MySQL預設開啟了SQL_MODE嚴格模式,對數據進行嚴格校驗。會報sql_mode=only_full_group_by錯誤說明寫的SQL語句不嚴謹,對於group by聚合操作,select中的列 ...
  • 原文鏈接:https://juejin.cn/post/7139572163371073543 項目準備 代碼、手冊 本文對應 2022 年的課程,Project 0 已經更新為實現字典樹了。C++17 的開發環境建議直接下載 CLion,不建議自己瞎折騰。 測試 $ mkdir build && ...
  • MySQL學習筆記 數據結構圖 更改字元集 alter database dbName character set 'charsetName'; alter database dwg2pdf character set 'utf8'; alter table tableName convert to ...
  • MySQL的用戶賬號: 由兩部分組成:用戶名和主機名 格式:'user_name'@'host' host必須要用引號括起來 註意:host可以是一個主機名也可以是具體的ip地址、網段等。 當host為主機名時: #例如: user1@'web1.redhat.org' 當host是ip地址或者網段 ...
  • 介紹弱隔離級別 為什麼要有弱隔離級別 如果兩個事務操作的是不同的數據, 即不存在數據依賴關係, 則它們可以安全地並行執行。但是當出現某個事務修改數據而另一個事務同時要讀取該數據, 或者兩個事務同時修改相同數據時, 就會出現併發問題。 在應用程式的開發中,我們通常會利用鎖進行併發控制,確保臨界區的資源 ...
  • 第一篇博客:HTML:iframe簡要介紹 前端 我們在寫網頁的時間,有許多重覆的界面,樣式和設計都一模一樣,為了避免代碼冗餘,我們通常把那些界面重覆的寫一個網頁,然後在需要的網頁進行引用那些重覆的界面,這時就需用到iframe。 1、iframe 定義和用法 : 1.iframe一般用來包含別的頁 ...
  • 1 別人寫的 地址鏈接 視頻鏈接: https://www.bilibili.com/video/BV1TK4y1Q78s github鏈接: https://github.com/Lavender-z/demo 如果上不了,就下個dev-sidecar代理 效果 代碼註釋 <!DOCTYPE ht ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...