1. 程式人生 > >深入理解單例模式

深入理解單例模式

在GoF的23種設計模式中,單例模式是比較簡單的一種。然而,有時候越是簡單的東西越容易出現問題。下面就單例設計模式詳細的探討一下。 所謂單例模式,簡單來說,就是在整個應用中保證只有一個類的例項存在。就像是Java Web中的application,也就是提供了一個全域性變數,用處相當廣泛,比如儲存全域性資料,實現全域性性的操作等。 1. 最簡單的實現 餓漢式 首先,能夠想到的最簡單的實現是,把類的建構函式寫成private的,從而保證別的類不能例項化此類,然後在類中提供一個靜態的例項並能夠返回給使用者。這樣,使用者就可以通過這個引用使用到這個類的例項了。 public class SingletonClass {

  private static final SingletonClass instance = new SingletonClass();
    
  public static SingletonClass getInstance() {
    return instance;
  }
    
  private
SingletonClass() {
    
  }
    
} 如上例,外部使用者如果需要使用SingletonClass的例項,只能通過getInstance()方法,並且它的構造方法是private的,這樣就保證了只能有一個物件存在。 2. 效能優化——lazy loaded 懶漢式 上面的程式碼雖然簡單,但是有一個問題——無論這個類是否被使用,都會建立一個instance物件。如果這個建立過程很耗時,比如需要連線10000次資料庫(誇張了…:-)),並且這個類還並不一定會被使用,那麼這個建立過程就是無用的。怎麼辦呢? 為了解決這個問題,我們想到了新的解決方案: public
class SingletonClass {

  private static SingletonClass instance = null;
    
  public static SingletonClass getInstance() {
    if(instance == null) {
      instance = new SingletonClass();
    }
    return instance;
  }
    
  private SingletonClass() {
    
  }
    
} 程式碼的變化有兩處——首先,把instance初始化為null,直到第一次使用的時候通過判斷是否為null來建立物件。因為建立過程不在宣告處,所以那個final的修飾必須去掉。 我們來想象一下這個過程。要使用SingletonClass,呼叫getInstance()方法。第一次的時候發現instance是null,然後就新建一個物件,返回出去;第二次再使用的時候,因為這個instance是static的,所以已經不是null了,因此不會再建立物件,直接將其返回。 這個過程就成為lazy loaded,也就是遲載入——直到使用的時候才進行載入。 3. 同步
上面的程式碼很清楚,也很簡單。然而就像那句名言:“80%的錯誤都是由20%程式碼優化引起的”。單執行緒下,這段程式碼沒有什麼問題,可是如果是多執行緒,麻煩就來了。我們來分析一下: 執行緒A希望使用SingletonClass,呼叫getInstance()方法。因為是第一次呼叫,A就發現instance是null的,於是它開始建立例項,就在這個時候,CPU發生時間片切換,執行緒B開始執行,它要使用SingletonClass,呼叫getInstance()方法,同樣檢測到instance是null——注意,這是在A檢測完之後切換的,也就是說A並沒有來得及建立物件——因此B開始建立。B建立完成後,切換到A繼續執行,因為它已經檢測完了,所以A不會再檢測一遍,它會直接建立物件。這樣,執行緒A和B各自擁有一個SingletonClass的物件——單例失敗! 解決的方法也很簡單,那就是加鎖: public class SingletonClass {

  private static SingletonClass instance = null;
    
  public synchronized static SingletonClass getInstance() {
    if(instance == null) {
      instance = new SingletonClass();
    }
    return instance;
  }
    
  private SingletonClass() {
    
  }
    
} 是要getInstance()加上同步鎖,一個執行緒必須等待另外一個執行緒建立完成後才能使用這個方法,這就保證了單例的唯一性。 4. 又是效能 上面的程式碼又是很清楚很簡單的,然而,簡單的東西往往不夠理想。這段程式碼毫無疑問存在效能的問題——synchronized修飾的同步塊可是要比一般的程式碼段慢上幾倍的!如果存在很多次getInstance()的呼叫,那效能問題就不得不考慮了! 讓我們來分析一下,究竟是整個方法都必須加鎖,還是僅僅其中某一句加鎖就足夠了?我們為什麼要加鎖呢?分析一下出現lazy loaded的那種情形的原因。原因就是檢測null的操作和建立物件的操作分離了。如果這兩個操作能夠原子地進行,那麼單例就已經保證了。於是,我們開始修改程式碼: public class SingletonClass {

  private static SingletonClass instance = null;
    
  public static SingletonClass getInstance() {
    synchronized (SingletonClass.class) {
      if(instance == null) {
        instance = new SingletonClass();
      }
    }    
    return instance;
  }
    
  private SingletonClass() {
    
  }
    
} 首先去掉getInstance()的同步操作,然後把同步鎖載入if語句上。但是這樣的修改起不到任何作用:因為每次呼叫getInstance()的時候必然要同步,效能問題還是存在。如果……如果我們事先判斷一下是不是為null再去同步呢? public class SingletonClass {

  private static SingletonClass instance = null;

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

  private SingletonClass() {

  }

} 還有問題嗎?首先判斷instance是不是為null,如果為null,加鎖初始化;如果不為null,直接返回instance。 這就是double-checked locking設計實現單例模式。到此為止,一切都很完美。我們用一種很聰明的方式實現了單例模式。 5. 從源頭檢查 下面我們開始說編譯原理。所謂編譯,就是把原始碼“翻譯”成目的碼——大多數是指機器程式碼——的過程。針對Java,它的目的碼不是本地機器程式碼,而是虛擬機器程式碼。編譯原理裡面有一個很重要的內容是編譯器優化。所謂編譯器優化是指,在不改變原來語義的情況下,通過調整語句順序,來讓程式執行的更快。這個過程成為reorder。 要知道,JVM只是一個標準,並不是實現。JVM中並沒有規定有關編譯器優化的內容,也就是說,JVM實現可以自由的進行編譯器優化。 下面來想一下,建立一個變數需要哪些步驟呢?一個是申請一塊記憶體,呼叫構造方法進行初始化操作,另一個是分配一個指標指向這塊記憶體。這兩個操作誰在前誰在後呢?JVM規範並沒有規定。那麼就存在這麼一種情況,JVM是先開闢出一塊記憶體,然後把指標指向這塊記憶體,最後呼叫構造方法進行初始化。 下面我們來考慮這麼一種情況:執行緒A開始建立SingletonClass的例項,此時執行緒B呼叫了getInstance()方法,首先判斷instance是否為null。按照我們上面所說的記憶體模型,A已經把instance指向了那塊記憶體,只是還沒有呼叫構造方法,因此B檢測到instance不為null,於是直接把instance返回了——問題出現了,儘管instance不為null,但它並沒有構造完成,就像一套房子已經給了你鑰匙,但你並不能住進去,因為裡面還沒有收拾。此時,如果B在A將instance構造完成之前就是用了這個例項,程式就會出現錯誤了! 於是,我們想到了下面的程式碼: public class SingletonClass {

  private static SingletonClass instance = null;

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

  private SingletonClass() {

  }
    
} 我們在第一個同步塊裡面建立一個臨時變數,然後使用這個臨時變數進行物件的建立,並且在最後把instance指標臨時變數的記憶體空間。寫出這種程式碼基於以下思想,即synchronized會起到一個程式碼遮蔽的作用,同步塊裡面的程式碼和外部的程式碼沒有聯絡。因此,在外部的同步塊裡面對臨時變數sc進行操作並不影響instance,所以外部類在instance=sc;之前檢測instance的時候,結果instance依然是null。 不過,這種想法完全是錯誤的!同步塊的釋放保證在此之前——也就是同步塊裡面——的操作必須完成,但是並不保證同步塊之後的操作不能因編譯器優化而調換到同步塊結束之前進行。因此,編譯器完全可以把instance=sc;這句移到內部同步塊裡面執行。這樣,程式又是錯誤的了! 6. 解決方案 說了這麼多,難道單例沒有辦法在Java中實現嗎?其實不然! 在JDK 5之後,Java使用了新的記憶體模型。volatile關鍵字有了明確的語義——在JDK1.5之前,volatile是個關鍵字,但是並沒有明確的規定其用途——被volatile修飾的寫變數不能和之前的讀寫程式碼調整,讀變數不能和之後的讀寫程式碼調整!因此,只要我們簡單的把instance加上volatile關鍵字就可以了。 public class SingletonClass {

  private volatile static SingletonClass instance = null;

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

  private SingletonClass() {

  }
    
} 然而,這只是JDK1.5之後的Java的解決方案,那之前版本呢?其實,還有另外的一種解決方案,並不會受到Java版本的影響: public class SingletonClass {
    
  private static class SingletonClassInstance {
    private static final SingletonClass instance = new SingletonClass();
  }

  public static SingletonClass getInstance() {
    return SingletonClassInstance.instance;
  }

  private SingletonClass() {

  }
    
} 在這一版本的單例模式實現程式碼中,我們使用了Java的靜態內部類。這一技術是被JVM明確說明了的,因此不存在任何二義性。在這段程式碼中,因為SingletonClass沒有static的屬性,因此並不會被初始化。直到呼叫getInstance()的時候,會首先載入SingletonClassInstance類,這個類有一個static的SingletonClass例項,因此需要呼叫SingletonClass的構造方法,然後getInstance()將把這個內部類的instance返回給使用者。由於這個instance是static的,因此並不會構造多次。 由於SingletonClassInstance是私有靜態內部類,所以不會被其他類知道,同樣,static語義也要求不會有多個例項存在。並且,JSL規範定義,類的構造必須是原子性的,非併發的,因此不需要加同步塊。同樣,由於這個構造是併發的,所以getInstance()也並不需要加同步。 至此,我們完整的瞭解了單例模式在Java語言中的時候,提出了兩種解決方案。個人偏向於第二種,並且Effiective Java也推薦的這種方式 最新的設計方案,採用enum方式,推薦:
public enum SingletonEnum {
    intance;
	public void sayHi(String name){
		System.out.println("Hello ,world !"+name);
	}
}
 
	public static void main(String[] args) {
		SingletonEnum.intance.sayHi("zhangsan");
	}
 

相關推薦

深入理解模式(上)

最近在閱讀《 》這本書,第3個條款專門提到了單例屬性,並給出了使用單例的最佳實踐建議。讓我對這個單例模式(原本我以為是設計模式中最簡單的一種)有了更深的認識。 單例模式 單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種型別的設計模式屬

深入理解模式:靜態內部類原理

這樣的 加載 hand 優點 傳遞 多個 喚醒 ref 一個   本文主要介紹java的單例模式,以及詳細剖析靜態內部類之所以能夠實現單例的原理。OK,廢話不多說,進入正文。    首先我們要先了解下單例的四大原則:    1.構造私有。    2.以靜態方法或者枚舉返回實

深入理解模式的幾種實現方式

前言 單例模式是一種很常用的設計模式,其定義是單例物件的類只允許有一個例項存在。在使用spring自動建立物件時預設就是單例的。 使用場景 需要頻繁的對物件進行建立與銷燬,如果工具類物件 一、餓漢式(靜態變數) public class Singleton1 { private st

深入理解模式——只有一個例項

目錄: 前言 初遇設計模式在上個寒假,當時把每個設計模式過了一遍,對設計模式有了一個最初級的瞭解。這個學期借了幾本設計模式的書籍看,聽了老師的設計模式課,對設計模式算是有個更進一步的認識。後面可能會不定期更新一下自己對於設計模式的理解。每個設計模式看似很簡單,

【Java】設計模式深入理解模式

什麼是設計模式?簡單的理解就是前人留下來的一些經驗總結而已,然後把這些經驗起了個名字叫Design Pattern,翻譯過來就是設計模式,通過使用設計模式可以讓我們的程式碼複用性更高,可維護性更高,讓你的程式碼寫的更優雅。設計模式理論上有23種,今天就先來

深入理解模式

在GoF的23種設計模式中,單例模式是比較簡單的一種。然而,有時候越是簡單的東西越容易出現問題。下面就單例設計模式詳細的探討一下。 所謂單例模式,簡單來說,就是在整個應用中保證只有一個類的例項存在。就像是Java Web中的application,也就是提供了一個全域性變數,用處相當廣泛,比如儲存全域性資

C++深入理解模式詳解

作者:知乎使用者連結:https://www.zhihu.com/question/27704562/answer/37760739來源:知乎著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。不使用編譯器擴充套件,不用C++11,不加鎖,也不使用原子操作的話

PHP 簡單理解模式和static 關鍵字

php單例模式 單例模式,是一種常用的軟體設計模式。在它的核心結構中只包含一個被稱為單例的特殊類。通過單例模式可以保證系統中一個類只有一個例項。即一個類只有一個物件例項。 要點主要有三個: 1.一個類只能有一個物件; 2.必須是自行建立這個類的物件; 3,要想整個系統提供

徹頭徹尾理解模式及其在多執行緒環境中的應用

摘要:      本文首先概述了單例模式產生動機,揭示了單例模式的本質和應用場景。緊接著,我們給出了單例模式在單執行緒環境下的兩種經典實現:餓漢式 和懶漢式,但是餓漢式是執行緒安全的,而懶漢式是非執行緒安全的。在多執行緒環境下,我們特別介紹了五種方式來在多執行緒環境下建立執行緒安全的單例,即分別使用sy

快速理解模式,工廠模式,代理模式三大模式

1.單例模式單例模式下,單例類只有一個,全域性內都可以直接呼叫靜態方法獲取到該單例的本體,然後呼叫該單例下的方法屬性2.工廠模式有一個工廠類,它負責幫你去快速生成指定的類,比如說,工廠有生產輪子,生產門,生產窗的三條流水線,你只需要告訴我你要生產輪子,工廠就幫你生產輪子到你手

JAVA理解模式

  單例模式,英文名為:Singleton pattern。首先,我們先去理解字面意思。Singleton:一個,獨身。pattern:模式,圖案,樣品。在字面上,可以理解為“一個樣品”。哈哈。再來看看

深入Java模式

在GoF的23種設計模式中,單例模式是比較簡單的一種。然而,有時候越是簡單的東西越容易出現問題。下面就單例設計模式詳細的探討一下。 所謂單例模式,簡單來說,就是在整個應用中保證只有一個類的例項存在。就像是Java Web中的application,也就是提供了一個全域性變數,用處相當廣泛,比如儲存全域性資

設計模式的簡單理解——模式

對象 troy 添加 reads 註釋 實例 [] 多線程 分配 簡單理解 單例模式是指進程生命期內,某個類型只實例化一個對象。這是一種通過語言特性實現的編程約束。如果沒有約束,那麽多人協同編碼時,就會出現非預期的情況。 下面以內存池做例子,假設其類型名為MemoryPoo

深入探討模式

最近學習了一下單例模式,看bilibili up主“狂神說Java”講完後,發現大部分部落格都少了一個很有趣的環節,不分享出來實在是太可惜了,原視訊 https://www.bilibili.com/video/BV1K54y197iS 1、瞭解單例 這個部分小部分我相信很多部落格都講的很好,我就儘量精簡

模式-解構函式的深入理解

singleton單例模式 單件模式      保證一個類中僅有一個例項,並且提供一個訪問他的全域性訪問點 a. 懶漢式:使用的時候才建立,多執行緒訪問的時候執行緒不安全(雙檢鎖) b. 餓漢式:類檔案載入的時候已經建立好了物件,如果物件一直沒有

模式深入理解(轉)

一. 什麼是單例模式因程式需要,有時我們只需要某個類同時保留一個物件,不希望有更多物件,此時,我們則應考慮單例模式的設計。二. 單例模式的特點1. 單例模式只能有一個例項。2. 單例類必須建立自己的唯一例項。3. 單例類必須向其他物件提供這一例項。三. 單例模式VS靜態類在知道了什麼是單例模式後,我想你一定會

深入理解PHP模式的實現&static&clone

前提 提到單例模式,那就不得不說設計模式。 單例模式是最簡單的一種設計模式,提供了一種唯一訪問其物件的方式,可以直接訪問。 屬於建立型模式 實現之前,我們先要明白兩個關鍵字的原理 static 宣告類屬性或方法為靜態,就可以不例項化類而直接訪問。靜態屬性不能通過一

深入理解 JavaScript 模式 (Singleton Pattern)

概念 單例模式,也叫單子模式,是一種常用的軟體設計模式。在應用這個模式時,單例物件的類必須保證只有一個例項存在。 核心:確保只有一個例項,並提供全域性訪問。 實現思路 一個類能返回物件一個引用(永遠是同一個)和一個獲得該例項的方法(必須是靜態方法,通常命名為getIntance);當我們呼叫這個方法時,類

快速理解Java中的五種模式

嵌套類 ati class 由於 aop 適合 singleton 重復 code 解法一:只適合單線程環境(不好) package test; /** * @author xiaoping * */ public class Singleton { pri

理解js設計模式模式

單例 false single 這樣的 字面量 不可靠 urn 如果 == 單例模式的定義:只提供唯一的一個實例來讓你訪問 js中單例是天然存在的: var a1={ hello:‘js‘ } var a2={ hello:‘js‘ } cons