結合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
更多相關文章
  • 一、HTML代碼如下: 二、jQuery代碼如下: ...
  • 剛看完JS中的深淺拷貝,來記錄分享一番,一起來開心的掉發吧。 首先瞭解深淺拷貝之前來看看JS中的幾種數據類型,分別有String、Number、Boolean、undefined、null、Object。es6還多了一種symbol,我們暫且先把他放一邊。前面提到的幾種數據類型前五種也就是Strin ...
  • <!DOCTYPE html><html> <head> <title></title> <style type="text/css"> html, body { height: 100%; width: 100%; margin: 0; padding: 0; position: relative ...
  • 在b站上看到一個大神用p5.js寫的,我覺得吧,原生是無敵的存在(其實是因為我不會),所以……效果自然沒有人家的好咯 js代碼: 請不要在意裡面的英文,當時只是覺得逼格很高 效果我一直不知道怎麼放 HTML中只要有個canvas就好了 ...
  • 學習web的第五天 CSS層疊樣式表,也稱為級聯樣式表,用來設計網頁風格。 老師說內聯樣式表不介意我們使用,我們只要瞭解並掌握就行了,重點給我們講解了外部樣式表。 css的優勢:1、表現和內容相分離,css通過定義html標記的樣式,使得頁面內容和顯示相分離,簡化了網頁格式設計。 2、加強了網頁的表 ...
  • v-bind:title="message" ...
  • 背景 去年年底的時候,靜兒在團隊會議中提出了自己的對整個服務將來的規劃。靜兒心裡明白自己的架構設想是可實現的,但是遠超目前的架構。被質疑無法落地。於是靜兒將一些概念的東西全都拋去,直接針對具體的項目做領域拆分。項目也在一點點像靜兒原來規劃的演進。 靜兒認為這個不做管理的一個好處:「對技術的挑戰要大的 ...
  • 1 IGame游戲公司的故事 1.1 討論會 話說有一個叫IGame的游戲公司,正在開發一款ARPG游戲(動作&角色扮演類游戲,如魔獸世界、夢幻西游這一類的游戲)。一般這類游戲都有一個基本的功能,就是打怪(玩家攻擊怪物,藉此獲得經驗、虛擬貨幣和虛擬裝備),並且根據玩家角色所裝備的武器不同,攻擊效果也 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...