你真的會寫單例模式嗎

来源:https://www.cnblogs.com/deppwang/archive/2020/04/10/12676818.html
-Advertisement-
Play Games

作者: "DeppWang" 、 "原文地址" 人生在世,誰不面試。單例模式:一個搞懂不加分,不搞懂減分的知識點 又一篇一抓一大把的博文,可是你真的的搞懂了嗎?點開看看。。事後,你也來一篇。 單例模式是面試中非常喜歡問的了,我們往往自認為已經完全理解了,沒什麼問題了。但要把它手寫出來的時候,可能出現 ...


作者:DeppWang原文地址

人生在世,誰不面試。單例模式:一個搞懂不加分,不搞懂減分的知識點

img

又一篇一抓一大把的博文,可是你真的的搞懂了嗎?點開看看。。事後,你也來一篇。

單例模式是面試中非常喜歡問的了,我們往往自認為已經完全理解了,沒什麼問題了。但要把它手寫出來的時候,可能出現各種小錯誤,下麵是我總結的快速準確的寫出單例模式的方法。

單例模式有各種寫法,什麼「雙重檢鎖法」、什麼「餓漢式」、什麼「飽漢式」,總是記不住、分不清。這就對了,人的記憶力是有限的,我們應該記的是最基本的單例模式怎麼寫。

單例模式:一個類有且只能有一個對象(實例)。單例模式的 3 個要點:

  1. 外部不能通過 new 關鍵字(構造函數)的方式新建實例,所以構造函數為私有:private Singleton(){}
  2. 只能通過類方法獲取實例,所以獲取實例的方法為公有、且為靜態:public static Singleton getInstance()
  3. 實例只能有一個,那隻能作為類變數的「數據」,類變數為靜態 (另一種記憶:靜態方法只能使用靜態變數):private static Singleton instance

一、最基礎、最簡單的寫法

類載入的時候就新建實例

public class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }
    
    public void show(){
        System.out.println("Singleon using static initialization in Java");
    }
}

// Here is how to access this Singleton class
Singleton.getInstance().show();

當執行 Singleton.getInstance() 時,類載入器載入 Singleton.class 進虛擬機,虛擬機在方法區(元數據區)為類變數分配一塊記憶體,並賦值為空。再執行 <client>() 方法,新建實例指向類變數 instance。這個過程在類載入階段執行,並由虛擬機保證線程安全。所以執行 getInstance() 前,實例就已經存在,所以 getInstance() 是線程安全的。

很多博文說 instance 還需要聲明為 final,其實不用。final 的作用在於不可變,使引用 instance 不能指向另一個實例,這裡用不上。當然,加上也沒問題。

這個寫法有一個不足之處,就是如果需要通過參數設置實例,則無法做到。舉個慄子:

class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() {
    }

    // 不能設置 name!
    public static Singleton getInstance(String name) {
        return instance;
    }
    
    public void show(){
        System.out.println("Singleon using static initialization in Java");
    }
}

// Here is how to access this Singleton class
Singleton.getInstance(String name).show();

二、可通過參數設置實例的寫法

考慮到這種情況,就在調用 getInstance() 方法時,再新建實例。

public class Singleton {
    private static Singleton instance;

    private String name;

    private Singleton(String name) {
        this.name = name;
    }

    public static synchronized Singleton getInstance(String name) {
        if (instance == null) {
            instance = new Singleton(name);
        }
        return instance;
    }

    public String show() {
        return name;
    }
}

Singleton.getInstance(String name).show();

這裡加了 synchronized 關鍵字,能保證只會生成一個實例,但效率不高。因為實例創建成功後,再獲取實例時就不用加鎖了。

當不加 synchronized 時,會發生什麼:

instance 是類的變數,類存放在方法區(元數據區),元數據區線程共用,所以類變數 instance 線程共用,類變數也是在主記憶體中。線程執行 getInstance() 時,在自己工作記憶體新建一個棧幀,將主記憶體的 instance 拷貝到工作記憶體。多個線程併發訪問時,都認為 instance == null,就將新建多個實例,那單例模式就不是單例模式了。

三、改良版加鎖的寫法

實現只在創建的時候加鎖,獲取時不加鎖。

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

為什麼要判斷兩次:

多個線程將 instance 拷貝進工作記憶體,即多個線程讀取到 instance == null,雖然每次只有一個線程進入 synchronized 方法,當進入線程成功新建了實例,synchronized 保證了可見性(在 unlock 操作前將變數寫回了主記憶體),此時 instance 不等於 null 了,但其他線程已經執行到 synchronized 這裡了,某個線程就又會進入 synchronized 方法,如果不判斷一次,又會再次新建一個實例。

為什麼要用 volatile 修飾 instance:

synchronized 可以實現原子性、可見性、有序性。其中實現原子性:一次只有一個線程執行同步塊的代碼。但電腦為了提升運行效率,會指令重排序。

代碼 instance = new Singleton(); 會被拆為 3 步執行。

  • A:分配一塊記憶體空間
  • B:在記憶體空間位置新建一個實例
  • C:將引用指向實例,即,引用存放實例的記憶體空間地址

如果 instance 都在 synchronized 裡面,那麼沒啥問題,問題出現在 instance 在 synchronized 外邊,因為此時外邊一群餓狼(線程),就在等待一個 instance 這塊肉不為 null。

模擬一下指令重排序的出錯場景:多線程環境下,正好一個線程,在同步塊中按 ACB 執行,執行到 AC 時(並將 instance 寫回了主記憶體),另一個線程執行第一個判斷時,認為 instance 不為空,返回 instance,但此時 instance 還沒被正確初始化,所以出錯。

當 instance 被 volatile 修飾時,只有 ACB 執行完了之後,其他線程才能讀取 instance

為什麼 volatile 能禁止指令重排序:它在 ACB 後添加一個 lock 指令,lock 指令之前的操作執行完成後,後面的操作才能執行

你可能認為上面的解釋太複雜,不好理解。對,確實比較複雜,我也搞了很久才搞明白。你可以看看這個是不是更好理解,Java 虛擬機規範的其中一條先行發生原則:對 volatile 修飾的變數,讀操作,必須等寫操作完成。

四、其他非主流寫法

枚舉寫法:

public enum EasySingleton{
    INSTANCE;
}

當面試官讓我寫一個單例模式,我總是覺得寫這個好像有點另類

靜態內部類寫法:

public class Singleton {  
    private static class SingletonHolder {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE; 
    }  
}

五、小結

單例模式主要為了節省記憶體開銷,Spring 容器的 Bean 就是通過單例模式創建出來的。

單例模式沒寫出來,那也沒啥事,因為那下一個問題你也不一定能答出來

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

-Advertisement-
Play Games
更多相關文章
  • 源代碼地址 通過對於鬥羅大陸小說的游戲化過程,熟悉Angular的結構以及使用TypeScript的面向對象開發方法。 "Github項目源代碼地址" 線上體驗網址(推薦使用移動設備訪問) http://datavisualization.club:8888/ 極簡游戲攻略 除了劇情對話之外,本游戲 ...
  • 原文鏈接:http://www.yiidian.com/jsp/jsp mvc model.html 1 什麼是MVC模式 MVC,代表模型(Model),視圖(View),控制器(Controller)。這是一種將業務邏輯,表現邏輯和數據分開的設計模式。 充當視圖和模型之間的介面。控制器攔截所有傳 ...
  • 以前用wordpress搭建的網站,顯示php版本需要更新了。 於是登錄寶塔面板 登錄後,選擇軟體商店 運行環境 選擇要安裝的PHP版本安裝 安裝完成後,選擇網站 設置 選擇PHP版本,切換為剛剛安裝好的版本,切換成功。 ...
  • java是一個面向對象的高級編程語言。 JDK發展史: 1995年Sun公司發佈Java 1.0 版本(初始化版本,出生的版本號) 1997年發佈Java 1.1版本 1998年發佈Java 1.2版本 2000年發佈Java 1.3版本 2002年發佈Java 1.4版本 2004年發佈Java ...
  • go語言基礎(一) package + package 調用 Go 程式是通過 package 來組織的。 只有 package 名稱為 main 的源碼文件可以包含 main 函數。 個可執行程式有且僅有一個 main 包。 通過 import 關鍵字來導入其他非 main 包。 可以通過 imp ...
  • public class CopyTextByBuf { public static void main(String[] args) { BufferedReader bufr = null; BufferedWriter bufw = null; try { bufr = new Buffere ...
  • 鏡像用法 修改 composer 的全局配置文件(推薦方式) 打開命令行視窗(windows用戶)或控制台(Linux、Mac 用戶)並執行如下命令: composer config -g repo.packagist composer https://packagist.phpcomposer.c ...
  • 緩衝區的出現提高了對數據的讀寫效率。 緩衝區要結合流才可以使用。 在流的基礎上對流的功能進行了增強。 該緩衝區提供了跨平臺的換行符。newLine(); public class BufferedWriterDemo { public static void main(String[] args) ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...