重新講講單例模式和幾種實現

来源:https://www.cnblogs.com/hyry/archive/2022/03/25/16056069.html
-Advertisement-
Play Games

一、什麼講單例模式 單例模式,最簡單的理解是對象實例只有孤單的一份,不會重覆創建實例。 這個模式已經很經典了,經典得我不再贅述理論,只給簡單註釋,畢竟教科書詳盡太多。 解決 sonar RSPEC-2168 異味的時候,發現目前業界推薦的單例模式和教科書上的已經有了較大差異,雙重鎖定不再推薦,甚至業 ...


目錄

    一、什麼講單例模式

    單例模式,最簡單的理解是對象實例只有孤單的一份,不會重覆創建實例。

    這個模式已經很經典了,經典得我不再贅述理論,只給簡單註釋,畢竟教科書詳盡太多。

    解決 sonar RSPEC-2168 異味的時候,發現目前業界推薦的單例模式和教科書上的已經有了較大差異,雙重鎖定不再推薦,甚至業內認為的最優方案不在sonar的推薦里

    於是提筆記錄,順帶補充了自己對多線程單例的理解 。

    二、經典的單線程單例

    這個部分沒有改動,簡單而經典,大致源碼如下

    public final class SignUtil {
    
        /**
         * 需要保持單例的對象
         */
        private static Object object;
    
        /**
         * 只允許SignUtil.getInstance獲取對象,也就是入口唯一
         */
        private SignUtil() {
        }
    
        /**
         * 對象的唯一齣口 調用時才初始化(懶載入)
         * @return Object 確保單線程情況下這裡出去就是初始化好的
         */
        public static Object getInstance() {
            if (null == object) {
                object = new Object();
            }
            return object;
        }
    
        /**
         * 內部函數也必須使用 getInstance這個入口
         */
        public static String getString() {
            return getInstance().toString();
        }
    }
    

    三、經典的雙重鎖定多線程單例 (JDK5-JDK7繼續適用)

    public final class SignUtil {
    
        /**
         * 需要保持單例的對象
         * 這裡需要聲明對象是易失的,因為object = new Object()不是一個原子操作,是被分拆為了實例化和初始化,一個申請空間,一個分配值
         * 那麼就有可能出現 C在第三瞬間進入getInstance函數,發現null!=object,此時對象實例化了但沒初始化就直接返回,是個高危操作
         */
        private volatile static Object object;
    
        /**
         * 只允許SignUtil.getInstance獲取對象,也就是入口唯一
         */
        private SignUtil() {
        }
    
        /**
         * 對象的唯一齣口
         *
         * @return Object 多線程情況下這裡出去就是初始化好的
         */
        public static Object getInstance() {
            // 第0瞬間 A B 兩個線程同時初始化,一看都是null嘛
            if (null == object) {
                // 第1瞬間 A B都進來了,因為不能重覆初始化,所以被synchronized鎖約束開始競爭.
                // A 贏了SignUtil的對象鎖,B 只能等著
                synchronized (SignUtil.class) {
                    // 這裡為什麼不直接object = new Object()呢?
                    // 因為B還等著呢,直接初始化就攔不住B再來一次初始化了.
                    if (null == object) {
                        // 第2瞬間, A終於初始化成功,且B不會重新初始化了.
                        object = new Object();
                        // 第3瞬間,因為object被volatile約束了,可以視為原子操作,補上最後一個漏洞,成功返回。
                    }
                }
            }
            return object;
        }
    
        /**
         * 內部函數也必須使用 getInstance這個入口
         */
        public static String getString() {
            return getInstance().toString();
        }
    }
    

    四、 JDK8 以後的多線程單例

    可以看到,三的要點太多了,很經典的雙重鎖定,但是不夠簡單優雅。目前更推薦下麵兩種格式

    4.1 JDK8 帶來的一個特性之一即是synchronized關鍵字,從原來的monitor重量級鎖,轉變成了由偏向鎖進行逐級升級到重量級鎖。換句話說,使用synchronized的代價被降低了,我們可以將上面的函數進行一個改進,讓它保持簡單和優雅。

    但是代價依舊存在,以下適合併發衝突不嚴重的項目。

    public final class SignUtil {
    
        /**
         * 需要保持單例的對象
         */
        private static Object object;
    
        /**
         * 只允許SignUtil.getInstance獲取對象,也就是入口唯一
         */
        private SignUtil() {
        }
    
        /**
         * 對象的唯一齣口 是的,僅比單線程版多了一個synchronized
         * @return Object 由於synchronized,同一瞬間只能有一個對象進行獲取實例
         */
        public static synchronized Object getInstance() {
            if (null == object) {
                object = new Object();
            }
            return object;
        }
    
        /**
         * 內部函數也必須使用 getInstance這個入口
         */
        public static String getString() {
            return getInstance().toString();
        }
    }
    

    4.2 利用靜態內部類的初始化特性

    很巧妙地利用了jvm的類載入機制。那就是靜態內部類的延遲載入性完成單例。

    public final class SignUtil {
    
        /**
         * 利用jvm的初始化規則 靜態內部類的靜態內部對象,只有在調用時才對靜態類開始初始化,
         * 類的初始化過程是線程安全的,所以也只有一個線程能進行初始化
         */
        private static class Node {
            /**
             * 在讀寫調用時才真正初始化,也就是懶載入
             */
            private static final Object object = new Object();
        }
    
        /**
         * 只允許SignUtil.getInstance獲取對象,也就是入口唯一
         */
        private SignUtil() {
        }
    
        /**
         * 不再是對象的唯一齣口,其他地方也只要讀寫都能完成初始化
         *
         * @return Object 調用時,會觸發內部靜態類的初始化,返回時,初始化已完成
         */
        public static Object getInstance() {
            return Node.object;
        }
    
        /**
         * 內部函數終於不用再依賴 getInstance這個入口
         */
        public static String getString() {
            return Node.object.toString();
        }
    }
    

    五、 有沒有辦法讓單例模式不單例?

    聽起來很魔鬼,但實際上,上述的多線程程單例都有兩個共同的缺陷可以做到:a 反射Constructor::setAccessible將私有構造函數改為公有函數 b.序列化時還是會返回多個實例。

    解決方法為改造構造函數和申明readResolve函數,參考如下,解決方案是通用的。

    public final class SignUtil {
    
        private static volatile boolean init = false;
    
        private static class Node {
            private static final Object object = new Object();
        }
    
        /**
         * 添加一個volatile的變數去判斷,防止反射初始化
         * 第二次初始化會拋出類強制轉換異常 當然你也可以用其他運行時異常
         */
        private SignUtil() {
            if (!init) {
                init = true;
            } else {
                throw new ClassCastException();
            }
        }
    
        public static Object getInstance() {
            return Node.object;
        }
    
        public static String getString() {
            return Node.object.toString();
        }
    
        /**
         * 反序列化時直接返回單例的對象,這麼寫的原因在 ObjectInputStream::readUnshared里
         */
        private Object readResolve() {
            return Node.object;
        }
    }
    

    六、枚舉單例
    6.1 單元素枚舉單例

    和4.2一樣,《Effective Java 》找到了另一種利用jvm類載入機制實現單例的方法:單元素枚舉單例。
    這裡有幾個前提:

    • Enum禁用了預設序列化。Enum::readObject、Enum::readObjectNoData約束了枚舉對象的預設反序列化,保證序列化安全
    • Enum提供了自己的序列化。Enum::toString 返回的是屬性名稱name,再通過Enum::valueOf把name轉回實例,保證了枚舉不會被“退貨”(這個直譯了,大概是final且不會被clone的意思)。
    • 這裡說一下valueOf的底層是Class::enumConstantDirectory,作用是調用時,生產一個Map<name, 枚舉>的映射,而這個map很像單線程單例模式,但他不是靜態共用變數,所以是線程安全的,

    不得不說,單元素枚舉的確成功避免了重重的繁瑣,但代價是沒有了懶載入的特性,變成了餓漢模式

    public enum SignUtil {
        /**
         * 從javap的反編譯結果看,會變成一個類公開的靜態變數,也就是餓漢模式
         * public static final SignUtil INSTANCE = new SignUtil();
         * 也就是會在載入類時直接初始化INSTANCE對象,而object對象是在構造時作為內部變數初始化,而構造函數是由jvm保證的
         */
        INSTANCE;
    
        /**
         * 由於INSTANCE單例,所以object才是單例的
         */
        private final Object object = new Object();
    
        public Object getInstance() {
            return object;
        }
    
        public String getString() {
            return object.toString();
        }
    
    }
    

    補一下javap反編譯後的結果

    public final class SignUtil extends java.lang.Enum<SignUtil> {
      public static final SignUtil INSTANCE;
      private final java.lang.Object object;
      private static final SignUtil[] $VALUES;
      public static SignUtil[] values();
      public static SignUtil valueOf(java.lang.String);
      private SignUtil(java.lang.Object);
      public java.lang.Object getInstance();
      public java.lang.String getString();
      static {};
    }
    

    6.2 多元素枚舉的單例呢?
    由於多元素枚舉的構造函數可以被反射修改成公用函數並設置object,但由於INSTANCE和object都是final約束的,所以修改就會報錯,以此保證了單例性。
    所以按照理解 多元素枚舉也能完成單例,只是適用場景偏少

    public enum SignUtil {
    	/*
    	 * 對的,唯一的區別就是由無參變成了有參構造,本質是不變的餓漢
    	 * public static final SignUtil INSTANCE = new SignUtil(new Object());
    	 */
        INSTANCE(new Object()),
        OTHER(new Object());
    
        private final Object object;
    
        private SignUtil(Object object) {
            this.object = object;
        }
    
        public Object getInstance() {
            return this.object;
        }
    
        public String getString() {
            return this.object.toString();
        }
    }
    

    您的分享是我們最大的動力!

    -Advertisement-
    Play Games
    更多相關文章
    • 鏡像下載、功能變數名稱解析、時間同步請點擊 阿裡雲開源鏡像站 linux配置yum源的三種方法: 1.配置網路yum源 2.通過上傳鏡像文件配置本地yum源 3.通過連接存儲或本地鏡像文件配置本地yum源(虛擬機) 配置各種yum源的時候,需要提前知道自己的linux系統是什麼版本的,以方便後面的配置 // ...
    • 鏡像下載、功能變數名稱解析、時間同步請點擊 阿裡雲開源鏡像站 安裝依賴包 yum install -y conntrack ntpdate ntp ipvsadm ipset jq iptables curl systat libseccomp wget vim net-tools git iptables ...
    • RadonDB MySQL Kubernetes 於 3 月 24 日正式發佈新版本 2.1.3 。該版本主要基於在 2.1.2 進行功能優化和升級。 致謝 首先感謝 @andyli029 @acekingke @runkecheng @mgw2168 @molliezhang 提交的修改。 什麼是 ...
    • 作者:程潤科 資料庫研發工程師 編輯:張莉梅 高級文檔工程師 視頻:錢芬 高級測試工程師 本文將演示在 Kubernetes 上部署 RadonDB MySQL Kubernetes 2.X(Operator)的步驟,快速實現 MySQL 高可用集群部署,以及部署集群的校驗和卸載方式。 部署版本為 ...
    • 近期熱播的電視劇《人世間》,講述了70年代無數普通人的故事,細膩的人物形象和真實的故事感動著我們。原來在那個年代,我們的父母和祖輩都在為新中國的美好生活而奮鬥著,為國家捨棄了小家團聚的機會;原來在那個年代,擁有一張合照也不是容易的事情。 多年來,隨著影像技術的迭代更新,人們的多彩生活被即時記錄著。同 ...
    • 前言 ​ 埋點一般可以分為客戶端埋點和後端埋點。由於客戶端埋點更加貼近業務,更加直觀,因此稱為市面上主流數據採集手段。對於 iOS 端的埋點,目前市面上主流代碼埋點和全埋點兩種方案。其中,代碼埋點即顯式地調用數據採集 SDK 提供的介面來採集數據,在採集能力上有比較大的優勢,但是需要做額外的開發,易 ...
    • Javascript中的for是如何實現迴圈的? 一、語法結構 單for迴圈 for (初始化變數; 條件表達式; 操作表達式) { 迴圈體 } 雙重for迴圈(也叫迴圈嵌套) for (外層初始化變數; 外層的表達式; 外層的操作表達式) { for (裡層的初始化變數; 裡層的條件表達式; 裡層 ...
    • 限時限次數點擊按鈕 思路: 用一個變數作為計數,點擊一次,計數加一 點擊函數內判斷計數變數 設置定時恢復 實現 HTML代碼 <body> <div class="a123"> <a class="btn bg1" onclick="doIt()">123123</a> <br> <div clas ...
    一周排行
      -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...