結合JDK源碼看設計模式——單例模式

来源:https://www.cnblogs.com/Cubemen/archive/2019/04/01/10639096.html
-Advertisement-
Play Games

定義: 保證一個類僅有一個實例,並提供一個全局訪問點 適用場景: 確保任何情況下這個對象只有一個實例 詳解: 1.私有構造器: 將本類的構造器私有化,其實這是單例的一個非常重要的步驟,沒有這個步驟,可以說你的就不是單例模式。這個步驟其實是防止外部函數在new的時候能構造出來新的對象,我們說單例要保證 ...


定義:

  保證一個類僅有一個實例,並提供一個全局訪問點

適用場景:

  確保任何情況下這個對象只有一個實例

詳解:

  1. 私有構造器
  2. 單利模式中的線程安全+延時載入
  3. 序列化和反序列化安全,
  4. 防止反射攻擊
  5. 結合JDK源碼分析設計模式

1.私有構造器:
  將本類的構造器私有化,其實這是單例的一個非常重要的步驟,沒有這個步驟,可以說你的就不是單例模式。這個步驟其實是防止外部函數在new的時候能構造出來新的對象,我們說單例要保證一個類只有一個實例,如果外部能new新的對象,那我們單例就是失敗的。所以無論什麼時候一定要將這個構造器私有化

2.單例模式中的線程安全+延時載入(懶漢式):

  其實從單線程角度來看,懶漢式是安全。這裡我們先來介紹一個線程安全的懶漢式接下來我們從三個版本的懶漢式來分析如何即做到線程安全又做到效率提高

  2.1原始版本

public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
if(lazySingleton != null){
throw new RuntimeException("單例構造器禁止反射調用");
}
}
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}

  我們來稍微分析一下為什麼線程不安全,現在有A,B兩個線程,假設兩個線程同時都走到了lazySingleton = new LazySingleton();這個創建對象的行,當都執行完的時候,就會創建兩個不同的對象然後分別返回。所以違背了單例模式的定義

  2.2加鎖

  可能很多人會直接在getInstance()方法上加一個synchronize關鍵字,這樣做完全可以但是效率會較慢,因為synchronize相當於鎖了整個對象,下麵的雙鎖結構就會比較輕量級一點

public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){

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

  可能很多人一眼就看見synchronize關鍵字位置變換了,鎖的範圍變小了,但是最關鍵的一個是private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;中的volatile關鍵字,因為如果不加這個關鍵字的時候,JVM會對沒有依賴關係的語句進行重排序,就是可能會線上程A的時候底層先設置lazyDoubleCheckSingleton 指向剛分配的記憶體地址,然後再來初始化對象,線程B呢線上程A設置lazyDoubleCheckSingleton 指向剛分配的記憶體地址完後就走到了第一個if,這時判斷是不為空的所以都沒有競爭synchronize中的資源就直接返回了,但是註意線程A並沒有初始化完對象,所以這時就會出錯。為瞭解決上述問題,我們可以引入volatile關鍵字,這個關鍵字是會有讀屏障寫屏障的,也就是由這個關鍵字修飾的變數,它中間的操作會額外加一層屏障來隔絕,詳情可以參考這篇博客。就會禁止一些操作的重排序。
  2.3靜態內部類

public class StaticInnerClassSingleton {
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton(){
if(InnerClass.staticInnerClassSingleton != null){
throw new RuntimeException("單例構造器禁止反射調用");
}
}


}

  我在類內部直接定義一個靜態內部類,在這個類需要載入的時候我直接把初始化的工作放在了靜態內部類中,當有幾個線程進來的時候,在class載入後被線程使用之前都是類的初始化階段,在這個階段JVM會獲取一個鎖,這個鎖可以同步多個線程對一個類的初始化,然後在內部類的初始化中會進行StaticInnerClassSingleton類的初始化。可以這麼理解,其實我們這個也是加了鎖,不過這是JVM內部加的鎖。

3.序列化與反序列化安全

  下麵先介紹一下餓漢式

public class HungrySingleton implements Serializable{

private final static HungrySingleton hungrySingleton;

static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
if(hungrySingleton != null){
throw new RuntimeException("單例構造器禁止反射調用");
}
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}

private Object readResolve(){
return hungrySingleton;
}

}

  餓漢式就是在類的初始化階段就已經載入好了,就算你不用這個對象,這個對象也已經創建好,不像懶漢式要等到要用的時候才載入。這是兩種模式的一個很大的區別,事實上餓漢式是線程安全的,就像懶漢式的內部類載入一樣,是由JVM加的鎖,但是兩者都不一定是序列化安全的。
  上面的餓漢式是序列化安全的,為什麼?因為多加了readResolve()方法。這時候有人會問為什麼要在餓漢式上多加一個這個方法。這裡的源碼我就不一一解析了。事實上在反序列化(從文件中讀取類)的時候,底層會有一個判斷。如果這個類在運行時是可序列化的,那麼我就會在讀取的時候創建一個新的類(反射創建),否則我就會讓這個類為空。再後面又有一個判斷,如果我的類這時候不為空,我就會通過反射嘗試調用readResolve()方法,然後最終返回給我的ObjectInputStream流。沒有的話我就返回之前創建的新對象。所以這就相當於覆蓋了之前讀取時候創建的類

4.防止反射攻擊

  看完上面的代碼你會發現,我基本上都在私有構造器中加入一個空判斷來拋出異常,反射攻擊的時候,上面的懶漢式中的內部類代碼和餓漢式中的序列化安全代碼都是可以防禦發射攻擊的,當然會拋出相應異常,接下來我們介紹一下枚舉單例模式

public enum EnumInstance {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance(){
return INSTANCE;
}

}

  枚舉對象不能被反射創建,並且序列化與反序列化中枚舉類型不會被創建出新的,下麵看看枚舉類型的構造器

protected Enum(String name,int ordinal){
this.name=name;
this.ordinal=ordinal;
}

  可見這個構造器是有參的,並且由這兩個值確定了枚舉唯一性,不會由序列化與反序列化破壞。並且也是線程安全的,原理同內部類。所以非常推薦枚舉類型來完成單例模式。
5.源碼解析:
  JDK中Runtime類就是一個單例模式,它不准外部創建實例,構造器代碼如下:

/** Don't let anyone else instantiate this class */
private Runtime() {}

  並且還是餓漢式,代碼如下:

private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}

  相信理解了上面的模式,可以很容易的明白這個類的設計模式

  當然還有我們常用的Spring框架,簡單說一下就是Spring中對象創建在Bean作用域中僅創建一個,和我們上面講的單例還是有稍許區別,這個單例的作用域是整個應用的上下文,通俗一點理解就是Spring就像一個商店,裡面的商品一種只有一個,大家看見的一個商品都是同一個,這一種商品中不會再有另一個商品了。


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

-Advertisement-
Play Games
更多相關文章
  • vue中v-text / v-html使用 顯示123 ...
  • 入門 ...
  • Demo asdasdasd <!DOCTYPE html> <html lang="en"> <head> <title>Demo</title> <style> #app{ width: 100px; height: 35px; background-color: #006600; text-a ...
  • 一、引言 大家都知道單例模式,通過一個全局變數來避免重覆創建對象而產生的消耗,若系統存在大量的相似對象時,又該如何處理?參照單例模式,可通過對象池緩存可共用的對象,避免創建多對象,儘可能減少記憶體的使用,提升性能,防止記憶體溢出。 在軟體開發過程,如果我們需要重覆使用某個對象的時候,如果我們重覆地使用n ...
  • 根據我的2019年 "個人發展計劃" ,要做一個自媒體,經過1個半月的精心準備,自媒體計劃終於正式啟動了,而這背後涵蓋了大量的思考、策劃、設計、準備和技術性的工作,伴隨著自媒體的內容輸出,我也把建設的過程記錄下來... 萬事開頭難,今天先寫第一部分也是最重要的部分 打地基 導讀 一、為什麼要做自媒體 ...
  • 前言 這一章的模板方法模式,個人感覺它是一個簡單,並且實用的設計模式,先說說它的定義: 模板方法模式定義了一個演算法的步驟,並允許子類別為一個或多個步驟提供其實踐方式。讓子類別在不改變演算法架構的情況下,重新定義演算法中的某些步驟。(百度百科) 額, 這段定義呢,如果說我在不瞭解這個設計模式的時候,我看著 ...
  • 進程——操作系統資源分配的最小單位 線程——是操作系統CPU調度的最小單位 進程 進程產生由來:電腦一次只能執行一個任務,如果某個任務需要從I/O設備讀取大量數據,此I/O操作過程中CPU是空閑的,可以用來進行其他操作。因此發明瞭進程,用進程來對應一個任務。 進程的特點:每個進程有獨立的記憶體空間, ...
  • 一、什麼是組合模式 定義:將對象以樹形結構組織起來,以達成“部分-整體”的層次結構,使得客戶端對單個對象和組合對象的使用具有一致性。 動機(Motivation) 客戶代碼過多地依賴於對象容器複雜的內部實現結構,對象容器內部實現結構(而非抽象介面)的變化將引起客戶代碼的頻繁變化,帶來了代碼的維護性、 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...