悟空模式-java-單例模式

来源:http://www.cnblogs.com/tirion/archive/2017/09/21/7562270.html
-Advertisement-
Play Games

【那座山,正當頂上,有一塊仙石。其石有三丈六尺五寸高,有二丈四尺圍圓。三丈六尺五寸高,按周天三百六十五度;二丈四尺圍圓,按政歷二十四氣。上有九竅八孔,按九宮八卦。四面更無樹木遮陰,左右倒有芝蘭相襯。蓋自開闢以來,每受天真地秀,日精月華,感之既久,遂有靈通之意。內育仙胞,一日迸裂,產一石卵,似圓球樣大 ...


【那座山,正當頂上,有一塊仙石。其石有三丈六尺五寸高,有二丈四尺圍圓。三丈六尺五寸高,按周天三百六十五度;二丈四尺圍圓,按政歷二十四氣。上有九竅八孔,按九宮八卦。四面更無樹木遮陰,左右倒有芝蘭相襯。蓋自開闢以來,每受天真地秀,日精月華,感之既久,遂有靈通之意。內育仙胞,一日迸裂,產一石卵,似圓球樣大。因見風,化作一個石猴,五官俱備,四肢皆全。便就學爬學走,拜了四方。目運兩道金光,射沖鬥府。】

上面這段文字,描述了悟空出生時的場景。孫悟空只有一個,任何程式要使用孫悟空這個對象,都只能使用同一個實例。

所以,單例模式非常好理解,單例模式確保一個類只有一個實例,且這個類自己創建自己的唯一實例並向整個系統提供這個實例,這個類叫做單例類。

其實,這個設計模式與抽象思維或者業務架構設計沒有太多關係,更多要求的是對Java記憶體模型以及併發編程的理解,所以在介紹單例模式之前,需要先介紹一下JMM(Java Memory Model)相關的基礎知識,然後再理解單例模式就會簡單得多。

1.重排序

在執行程式時,為了提高性能,編譯器和處理器常常會對指令做重排序。重排序又包括編譯器優化的重排序、指令級並行的重排序以及記憶體系統的重排序。

比如下麵一段代碼:

int a = 1;//A
int b = 2;//B

A並不一定是比B先執行的,它們的執行順序可能是A-B,也可能是B-A,甚至有可能是一同執行的。

2.happens-before與as-if-serial

as-if-serial保證單線程內程式的執行結果不被改變,它給程式員一個幻覺:單線程程式是按程式的順序來執行的;

happens-before保證正確同步的多線程程式的執行結果不被改變,它給程式員一個幻覺:正確同步的多線程程式是按happens-before指定的順序來執行的。

程式員其實並不關心兩個指令是否真的被重排序了,我們只關心程式執行的語義不能被改變,也就是程式的執行結果不能改變。

比如上面那段代碼的A與B順序顛倒過來,對程式的結果並沒有影響,我們還是可以獲得兩個賦值正確的int變數。但如果是下麵這段代碼,就有問題了:

int x = 1;//A
int x = 2;//B

如果這兩行代碼的執行順序發生了改變,那麼我們最終得到的x的值可能不是2,而是1,那樣程式的執行結果就發生了改變了。好在JMM對於這種有數據依賴性(兩個指令都是對同一個變數進行的)的重排序已經禁止了,所以我們並不需要擔心。

3.類初始化鎖

Java語言規範規定,對於每一個類或者介面A,都有一個唯一的初始化鎖LA與之對應。從A到LA的映射,由JVM的具體實現去自由實現。JVM在類初始化期間會獲取這個初始化鎖,並且每個線程至少獲取一次鎖來確保這個類已經被初始化過了。這個鎖可以同步多個線程對同一個類的初始化

4.volatile的記憶體語義

當寫一個volatile變數時,JMM會把該線程對應的本地記憶體中的共用變數值刷新到主記憶體;當讀一個volatile變數時,JMM會把該線程對應的本地記憶體置為無效,線程接下來將從主記憶體中讀取共用變數。

5.JSR-133記憶體模型

JDK5開始,java升級了記憶體模型,開始使用JSR-133記憶體模型。JSR-133對舊記憶體模型的修補主要有兩個:增強volatile的記憶體語義,嚴格限制volatile變數與普通變數的重排序(舊記憶體模型允許volatile變數與普通變數重排序);增強final的記憶體語義,使final具有初始化安全性,在舊的記憶體模型中,多次讀取同一個final變數的值可能會不同。

 

下麵我們再來開始看單例模式的各種實現方式,也許你還對上面這些概念不是很熟悉,但結合具體的代碼,相信會加深你的理解。

餓漢模式

package com.tirion.design.singleton;

public class WuKong {
    private static WuKong wuKong = new WuKong();

    private WuKong() {
    }

    public static WuKong getWuKong() {
        return wuKong;
    }
}

static變數會在類裝載的時候完成初始化,這裡註意構造方法也被聲明為private,我們只能通過WuKong.getWuKong()來獲取WuKong的唯一實例wuKong靜態變數。

因為單例的實現是在類裝載的時候完成的,並且無論後面對象實例是否被真正用到(WuKong.getWuKong()會不會得到執行),對象實例都已經被創建了,所以把這種以空間換時間的方式成為餓漢模式。

餓漢模式的優缺點也非常明顯,它不必等到用到的時候再創建實例,節省了程式的運行時間,但在某些情況下也可能創建了不必要的對象,導致空間被浪費。

懶漢模式

package com.tirion.design.singleton;

public class WuKong {
    private static WuKong wuKong = null;

    private WuKong() {
    }

    public static synchronized WuKong getWuKong() {
        if (wuKong == null) {
            wuKong = new WuKong();
        }
        return wuKong;
    }
}

懶漢模式與餓漢模式的不同之處在於把實例對象的創建放到了靜態工廠方法內部,當調用WuKong.getWuKong()時,會判斷實例是否已經被創建,如果沒有創建則進行實例對象的初始化工作,已經創建則直接返回。

懶漢模式為了實現多線程環境下的線程安全,在創建實例的方法上增加了synchronized同步控制,順便說一下synchronized是編譯器通過插入monitorenter和monitorexit指令來進行同步控制的,所有調用synchronized方法的線程都要在monitorenter處等待獲取monitor對象鎖,所以導致懶漢模式線上程競爭環境下效率非常低,這也是稱之為懶漢模式的原因。

基於volatile的DCL雙重檢查鎖機制的單例

 1 package com.tirion.design.singleton;
 2 
 3 public class WuKong {
 4     private static volatile WuKong wuKong = null;
 5 
 6     private WuKong() {
 7     }
 8 
 9     public static WuKong getWuKong() {
10         if (wuKong == null) {
11             synchronized (WuKong.class) {
12                 if (wuKong == null) {
13                     wuKong = new WuKong();
14                 }
15             }
16         }
17         return wuKong;
18     }
19 }

我們發現,雙重鎖檢查機制相比於懶漢模式,又有幾個細節被改動:

a.靜態工廠方法的synchronized被去掉了,改為使用同步代碼塊來進行控制

b.從原先的一次判斷對象實例是否為null改為了兩次判斷

c.對象實例增加了volatile關鍵詞修飾

下麵我們來對這幾個細節一一進行分析,看看這些改動有哪些意義:

針對第一個改動,我們從懶漢模式的分析中已經可以看出,synchronized方法的效率會比較差,實際情況下,除了對象實例剛剛要被創建及正在被創建的那段時間里,後面的時間針對synchronized同步鎖的競爭都是浪費的(因為對象實例已經被建立了),所以這裡通過第一個判斷 if (wuKong == null){synchronized...},規避了對象實例被創建後的所有對synchronized的同步鎖競爭,大大節省了代碼的執行時間,提高了效率;

針對第二個改動,是結合上一個改動而產生的,想象現在有兩個線程A和B同時進入了Line9(代碼行號)方法,由於它倆是前兩個進入方法的,所以它們都通過了Line10的對象實例為空的判斷,進入了Line11的同步代碼塊,由於同一時間只有一個線程能夠進入同步代碼塊,所以線程A獲得了監視器鎖,進入了同步代碼塊內部並執行了對象實例的初始化工作,當線程A退出同步代碼塊時會釋放監視器鎖,這時處於Blocked狀態下的線程B就會獲取到監視器鎖併進入到同步代碼塊中,如果沒有第二個實例對象是否為空的判斷的話,線程B就也會執行一遍對象實例的初始化,這樣就違反單例模式對象實例只初始化一次的原則了;

針對第三個改動,我們先要看一下JVM是如何執行Line13的wuKong = new WuKong()這段代碼的,其實,這一行代碼可以分解為如下的三行偽代碼:

memory = allocate();   // 1-分配對象的記憶體空間
ctorInstance(memory);  // 2-初始化對象
wuKong = memory;       // 3-設置wuKong指向剛分配的記憶體地址

在一些編譯器上,上面三行代碼中的2和3可能會發生重排序,因為重排序並不影響as-if-serial原則,重排序後,就是先把wuKong這個實例指向空的記憶體空間地址,隨後再在空的記憶體空間上進行對象的初始化工作。

在單線程的情況下,上述重排序確實不會影響程式的執行結果,但在多線程環境下,可能會出現如下情況:

線程B剛剛進入Line10的is null判斷時,線程A恰好出現了對象記憶體地址分配與對象初始化的重排序,這時候線程B看到的對象實例不是null(空的記憶體地址,但不是null),所以線程B直接繞過了同步代碼塊,直接返回了一個還未進行初始化的對象。

那麼我們如何解決這個問題呢?一種思路是禁止對象記憶體地址指向和對象初始化的重排序。

在JDK5或更高版本後,Java開始使用了新的JSR-133記憶體模型,在這個模型中對舊記憶體模型做了一個重要的修補,增強了volatile關鍵字的記憶體語義,通過添加記憶體屏障的方式,禁止了volatile對象初始化與記憶體地址指向的重排序,也因此避免了上述情況可能導致的問題。

需要註意的是,這個解決方案只在JDK5及之後才能正常運作。

基於類初始化的單例

package com.tirion.design.singleton;

public class WuKong {

    private WuKong() {
    };

    private static class WuKongHolder {
        public static WuKong wuKong = new WuKong();
    }

    public static WuKong getWuKong() {
        return WuKongHolder.wuKong;
    }

}

在調用WuKong.getWuKong()時,WuKongHolder將被立即初始化,在上面我們已經介紹了類初始化時,所有線程都會去競爭一個類初始化鎖,所以這個初始化動作是線程安全的。

同時,在第一個線程完成類的初始化寫入工作,釋放類初始化鎖的之後,第二個線程會嘗試獲取這個類初始化鎖,happens-before規則保證了一個鎖的釋放一定發生在同一個鎖的獲取之前,所以第一個線程在釋放鎖之前執行類的初始化的寫入操作對後面獲取同一個鎖的線程可見。

在happens-before規則的保證下,無論WuKong wuKong = new WuKong();代碼內部發生了怎樣的重排序,對於後面的線程來說都不可見。

通過對比基於volatile的雙重檢查鎖定的單例和基於類初始化的單例,我們發現基於類初始化的方案的實現代碼更加簡潔方便,也不需要太多的JMM知識。

但是基於volatile的DCL的單例模式有一個額外的優勢,就是除了可以對靜態欄位實現延遲初始化之外,還可以對實例欄位實現延遲初始化。所以當需要對實例欄位實現延遲初始化的時候,可以選擇基於volatile的雙重檢查機制的單例模式。

基於枚舉的單例

package com.tirion.design.singleton;

public enum WuKongEnum {

    WUKONG;

    private WuKong wuKong;

    private WuKongEnum() {
        wuKong = new WuKong();
    }

    public WuKong getWuKong() {
        return wuKong;
    }
}

在理解基於枚舉的單例之前,我們先要知道編譯器會在創建枚舉時替我們創建一個繼承java.lang.Enum的類,這個創建過程我們是無法干涉的,這個類看起來像下麵這樣

public class WuKongEnum extends Enum{
       public static final WuKongEnum WUKONG;
       ...  
}

在調用WuKongEnum.getWuKong()時,編譯器自動生成的private構造方法將得到執行,對象實例將得到初始化,另外由於對象實例是static final的,所以JVM將會保證它只會初始化一次。另外Enum實現了Serializable介面,所以它也無償提供了序列化機制。

所以說,用枚舉實現單例模式是簡潔、高效且安全的。

關於單例模式的介紹就到這裡,你可以將它記憶為悟空單例模式

如果你認為文章中哪裡有錯誤或者不足的地方,歡迎在評論區指出,也希望這篇文章對你學習java設計模式能夠有所幫助。轉載請註明,謝謝。

更多設計模式的介紹請到悟空模式-java設計模式中查看。


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

-Advertisement-
Play Games
更多相關文章
  • 如圖: 第一步:分析行數 4行 迴圈列印4層 第二步:分析 * 的個數 1->3->5->7 2*n-1 第三步:彙總列印直角三角形 第四步: 觀察 (2*n-2)/2 3->2->1 列印0的直角倒三角 第五步:彙總列印等腰三角形 最後將0替換成" "(空格),編譯再運行就好了 ...
  • 使用python web做Restful 風格,很簡單,採用Flask框架輕鬆實現一個RESTful的服務。 Restful相關介紹請查看:https://www.ibm.com/developerworks/library/ws-restful/index.html 1. 環境搭建 首先需要準備環 ...
  • JdbcUtils工具類3.0最終版,添加了事務相關功能和釋放鏈接。最終版本可以直接打成jar包,在後面的基本項目都會使用該工具類 1. JdbcUtils代碼 2. 在src下給出c3p0-config.xml配置文件 3. 總結 從第一個基本版本1.0到加入連接池2.0再到現在的事務,一步一個腳 ...
  • 算術異常類:ArithmeticExecption 空指針異常類:NullPointerException 類型強制轉換異常:ClassCastException 數組負下標異常:NegativeArrayException 數組下標越界異常:ArrayIndexOutOfBoundsExcepti ...
  • 在《基於Spring Boot,使用JPA操作Sql Server資料庫完成CRUD》,《基於Spring Boot,使用JPA調用Sql Server資料庫的存儲過程並返回記錄集合》完成了CRUD,調用存儲過程查詢數據。 很多複雜的情況下,會存在要直接執行SQL來獲取數據。 通過“EntityMa ...
  • 標題示例: 標題一 標題二 標題三 標題四 標題五 標題六 連接示例: " " "github" 無序列表示例: 1 2 3 4 代碼示例: 1、一行代碼用``包含 2、多行代碼用一對 { "code":1, "message":"成功", "object":{ }, "map":{}, "hand ...
  • 如果準備工作: 1.Python 2.Django 3.Git 安裝Python: 官網下載 安裝Django: 現在正式開始創建一個名為my_blog的Django項目: 建立Django app(article): 到目前為止的項目結構如下: 併在my_blog/my_blog/setting. ...
  • 1.什麼是用例? 用例模型主要應用在工程開發的初期進行系統需求分析階段,描述了系統具備什麼功能,也就是說從用戶的角度觀察系統應該支持哪些功能,同時幫助系統分析員對系統功能有個全面的認識,從巨集觀上描述系統的行為。 用例模型包括的基本元素有:用例,角色,系統。 2用例的作用 一個系統中可以包含多個用例, ...
一周排行
    -Advertisement-
    Play Games
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...