1. 程式人生 > >【Java】設計模式:深入理解單例模式

【Java】設計模式:深入理解單例模式

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

引言

對於單例模式,有工作經驗的人基本上都使用過。面試的時候提到設計模式基本上都會提到單例模式,但是很多人對單例模式也是一知半解,當然也包括我哈哈哈=_=。所以我們有必要深入理解一下所謂的「單例模式」。

單例模式

定義:保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點。

單例模式結構圖:

這裡寫圖片描述

使用單例的優點:

  • 單例類只有一個例項
  • 共享資源,全域性使用
  • 節省建立時間,提高效能

它的七種寫法

單例模式有多種寫法各有利弊,現在我們來看看各種模式寫法。

1、餓漢式

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

這種方式和名字很貼切,飢不擇食,在類裝載的時候就建立,不管你用不用,先建立了再說,如果一直沒有被使用,便浪費了空間,典型的空間換時間,每次呼叫的時候,就不需要再判斷,節省了執行時間。

Java Runtime就是使用這種方式,它的原始碼如下:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return
the <code>Runtime</code> object associated with the current * Java application. */
public static Runtime getRuntime() { return currentRuntime; } /** Don't let anyone else instantiate this class */ private Runtime() {} //以下程式碼省略 }

總結:「餓漢式」是最簡單的實現方式,這種實現方式適合那些在初始化時就要用到單例的情況,這種方式簡單粗暴,如果單例物件初始化非常快,而且佔用記憶體非常小的時候這種方式是比較合適的,可以直接在應用啟動時載入並初始化。

但是,如果單例初始化的操作耗時比較長而應用對於啟動速度又有要求,或者單例的佔用記憶體比較大,再或者單例只是在某個特定場景的情況下才會被使用,而一般情況下是不會使用時,使用「餓漢式」的單例模式就是不合適的,這時候就需要用到「懶漢式」的方式去按需延遲載入單例。

2、懶漢式(非執行緒安全)

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

懶漢模式申明瞭一個靜態物件,在使用者第一次呼叫時初始化,雖然節約了資源,但第一次載入時需要例項化,反映稍慢一些,而且在多執行緒不能正常工作。在多執行緒訪問的時候,很可能會造成多次例項化,就不再是單例了。

「懶漢式」與「餓漢式」的最大區別就是將單例的初始化操作,延遲到需要的時候才進行,這樣做在某些場合中有很大用處。比如某個單例用的次數不是很多,但是這個單例提供的功能又非常複雜,而且載入和初始化要消耗大量的資源,這個時候使用「懶漢式」就是非常不錯的選擇。

3、懶漢式(執行緒安全)

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

這兩種「懶漢式」單例,名字起的也很貼切,一直等到物件例項化的時候才會建立,確實夠懶,不用鞭子抽就不知道走了,典型的時間換空間,每次獲取例項的時候才會判斷,看是否需要建立,浪費判斷時間,如果一直沒有被使用,就不會被建立,節省空間。

因為這種方式在getInstance()方法上加了同步鎖,所以在多執行緒情況下會造成執行緒阻塞,把大量的執行緒鎖在外面,只有一個執行緒執行完畢才會執行下一個執行緒。

Android中的 InputMethodManager 使用了這種方式,我們看看它的原始碼:

public final class InputMethodManager {

    static InputMethodManager sInstance;

     /**
     * Retrieve the global InputMethodManager instance, creating it if it
     * doesn't already exist.
     * @hide
     */
    public static InputMethodManager getInstance() {
        synchronized (InputMethodManager.class) {
            if (sInstance == null) {
                IBinder b = ServiceManager.getService(Context.INPUT_METHOD_SERVICE);
                IInputMethodManager service = IInputMethodManager.Stub.asInterface(b);
                sInstance = new InputMethodManager(service, Looper.getMainLooper());
            }
            return sInstance;
        }
    }
}

4、雙重校驗鎖(DCL)

上面的方法「懶漢式(執行緒安全)」毫無疑問存在效能的問題 — 如果存在很多次getInstance()的呼叫,那效能問題就不得不考慮了!

讓我們來分析一下,究竟是整個方法都必須加鎖,還是僅僅其中某一句加鎖就足夠了?我們為什麼要加鎖呢?分析一下出現lazy loaded的那種情形的原因。原因就是檢測null的操作和建立物件的操作分離了。如果這兩個操作能夠原子地進行,那麼單例就已經保證了。於是,我們開始修改程式碼,就成了下面的雙重校驗鎖(Double Check Lock):

public class Singleton {

    /**
     * 注意此處使用的關鍵字 volatile,
     * 被volatile修飾的變數的值,將不會被本地執行緒快取,
     * 所有對該變數的讀寫都是直接操作共享記憶體,從而確保多個執行緒能正確的處理該變數。
     */
    private volatile static Singleton singleton;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return singleton;
    }
}

這種寫法在getSingleton()方法中對singleton進行了兩次判空,第一次是為了不必要的同步,第二次是在singleton等於null的情況下才建立例項。在這裡用到了volatile關鍵字,不瞭解volatile關鍵字的可以檢視 Java多執行緒(三)volatile域 java中volatile關鍵字的含義 兩篇文章,可以看到雙重檢查模式是正確使用volatile關鍵字的場景之一。

「雙重校驗鎖」:既可以達到執行緒安全,也可以使效能不受很大的影響,換句話說在保證執行緒安全的前提下,既節省空間也節省了時間,集合了「餓漢式」和兩種「懶漢式」的優點,取其精華,去其槽粕。

對於volatile關鍵字,還是存在很多爭議的。由於volatile關鍵字可能會遮蔽掉虛擬機器中一些必要的程式碼優化,所以執行效率並不是很高。也就是說,雖然可以使用“雙重檢查加鎖”機制來實現執行緒安全的單例,但並不建議大量採用,可以根據情況來選用。

還有就是在java1.4及以前版本中,很多JVM對於volatile關鍵字的實現的問題,會導致“雙重檢查加鎖”的失敗,因此“雙重檢查加鎖”機制只只能用在java1.5及以上的版本。

5、靜態內部類

另外,在很多情況下JVM已經為我們提供了同步控制,比如:

  • static {...}區塊中初始化的資料
  • 訪問final欄位時

因為在JVM進行類載入的時候他會保證資料是同步的,我們可以這樣實現:採用內部類,在這個內部類裡面去建立物件例項。這樣的話,只要應用中不使用內部類 JVM 就不會去載入這個單例類,也就不會建立單例物件,從而實現「懶漢式」的延遲載入和執行緒安全。

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

第一次載入Singleton類時並不會初始化sInstance,只有第一次呼叫getInstance方法時虛擬機器載入SingletonHolder 並初始化sInstance ,這樣不僅能確保執行緒安全也能保證Singleton類的唯一性,所以推薦使用靜態內部類單例模式。

然而這還不是最簡單的方式,《Effective Java》中作者推薦了一種更簡潔方便的使用方式,就是使用「列舉」。

6、列舉

《Java與模式》中,作者這樣寫道,使用列舉來實現單例項控制會更加簡潔,而且無償地提供了序列化機制,並由JVM從根本上提供保障,絕對防止多次例項化,是更簡潔、高效、安全的實現單例的方式。

public enum Singleton {
     //定義一個列舉的元素,它就是 Singleton 的一個例項
     INSTANCE;  

     public void doSomeThing() {  
         // do something...
     }  
 } 

使用方法如下:

public static void main(String args[]) {
    Singleton singleton = Singleton.instance;
    singleton.doSomeThing();
}

列舉單例的優點就是簡單,但是大部分應用開發很少用列舉,可讀性並不是很高,不建議用。

7. 使用容器

public class SingletonManager { 
  private static Map<String, Object> objMap = new HashMap<String,Object>();
  private Singleton() { 
  }
  public static void registerService(String key, Objectinstance) {
    if (!objMap.containsKey(key) ) {
      objMap.put(key, instance) ;
    }
  }
  public static ObjectgetService(String key) {
    return objMap.get(key) ;
  }
}

這種使用SingletonManager 將多種單例類統一管理,在使用時根據key獲取物件對應型別的物件。這種方式使得我們可以管理多種型別的單例,並且在使用時可以通過統一的介面進行獲取操作,降低了使用者的使用成本,也對使用者隱藏了具體實現,降低了耦合度。

總結

對於以上七種單例,分別是「餓漢」、「懶漢(非執行緒安全)」、「懶漢(執行緒安全)」、「雙重校驗鎖」、「靜態內部類」、「列舉」和「容器類管理」。很多時候取決人個人的喜好,雖然雙重檢查有一定的弊端和問題,但我就是鍾愛雙重檢查,覺得這種方式可讀性高、安全、優雅(個人觀點)。所以程式碼裡常常默寫這樣的單例,寫的時候真感覺自己是個偉大的建築師哈哈哈哈(真不要臉(¬_¬)(逃。

嘻嘻嘻

【參考資料】:

相關推薦

Java設計模式深入理解模式

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

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

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

深入理解模式(上)

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

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

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

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

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

深入理解模式

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

C++深入理解模式詳解

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

Android小白進階之模式淺析

1、基礎簡介 由於最近專案需求使用到了IO操作,特意花費一定的時間研究了下單例模式,希望對你有用。 定義: 確保某個類只有一個例項,而且自行例項化提供給外部使用。 使用場景: 某個型別的物件只應該有且只有一個,或者避免建立多個物件消耗過多的資源時。 例如: 訪問

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

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

JAVA設計模式模式

懶漢 常見 單例 str 自己 餓漢式 span color 實例 前言 java最常見的設計模式就是單例模式,而單例模式最常見的就是懶漢式和餓漢式,現在就分別看一下 1.懶漢式 懶漢式比較懶,當別人用到時才會初始化實例,而當有多個人同時用到就可能產生多個實例,造成線程

Java併發指南2深入理解Java記憶體模型JMM

一:JMM基礎與happens-before 1併發程式設計模型的分類 1.1執行緒之間如何通訊及執行緒之間如何同步 1.11執行緒之間的通訊機制 通訊:執行緒之間以何種機制來交換資訊 通訊機制有兩種:共享記憶體和訊息傳遞。 共享記憶體併發模型 執行

Java知乎學習 JAVA,有什麼書籍推薦?學習的方法和過程是怎樣的?

現在網際網路上資源豐富,Java 學習並不難。貼個 Java 服務端入門和進階指南,是給我們組新人入門用的,包括了學習目標、需要掌握的技能和參考資料,並規劃了學習階段和時間,希望幫助到題主。 前言 歡迎加入我們。這是一份針對實習生/畢業生的服務端開發入門與進階指南。遇到問題及時問你的 mentor 或者直接問

二、JAVA多執行緒深入理解Thread建構函式(Thread、Runnable、守護執行緒、ThreadGroup)

本章主要介紹了所有與Thread有關的建構函式,執行緒的父子關係(並非繼承關係,而是一種包含關係),Thread和ThreadGroup之間的關係,Thread與虛擬機器棧的關係(學習這部分內容需要讀者有JVM的相關基礎,尤其是對棧記憶體要有深入的理解),最後還介紹了守護執行緒的概念、特點和使用場景

從頭學習設計模式(一)——模式

一、引入   單例模式作為23種設計模式中的最基礎的一種模式,在平時開發中應用也非常普遍。到底哪些類應該設計成單例的呢,我們來舉個最通俗的例子。在一個父容器中單擊某個選單項開啟一個子視窗,如果不使用單例又沒有作選單項的可用控制的話,每次單擊選單項都會開啟一個新視窗。這不僅會浪費記憶體資源,在程式邏輯

夯實Java基礎系列9深入理解Class類和Object類

目錄 Java中Class類及用法 Class類原理 如何獲得一個Class類物件 使用Class類的物件來生成目標類的例項 Object類 類構造器public Object(); registerNatives()方法; Clone()方法實現淺拷貝 getClass()方法 equals()方法

夯實Java基礎系列10深入理解Java中的異常體系

目錄 為什麼要使用異常 異常基本定義 異常體系 初識異常 異常和錯誤 異常的處理方式 "不負責任"的throws 糾結的finally throw : JRE也使用的關鍵字 異常呼叫鏈 自定義異常 異常的注意事項 當finally遇上return JAVA異常常見面試題 參考文章 微信公眾號 Java技術

夯實Java基礎系列11深入理解Java中的回撥機制

目錄 模組間的呼叫 多執行緒中的“回撥” Java回撥機制實戰 例項一 : 同步呼叫 例項二:由淺入深 例項三:Tom做題 參考文章

夯實Java基礎系列12深入理解Java中的反射機制

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視 https://github.com/h2pl/Java-Tutorial 喜歡的話麻煩點下Star哈 文章首發於我的個人部落格: www.how2playlife.com 列舉(enum)型別是Java

Java基礎系列6深入理解Java異常體系

該系列博文會告訴你如何從入門到進階,一步步地學習Java基礎知識,並上手進行實戰,接著瞭解每個Java知識點背後的實現原理,更完整地瞭解整個Java技術體系,形成自己的知識框架。   前言: Java的基本理念是“結構不佳的程式碼不能執行”。 “異常”這個詞有“我對此感到意外”的意思。問題出現了,你

java設計模式---(1)模式

單例模式 定義: 確保一個類只有一個例項, 而且自行例項化並向整個系統提供這個例項 保證只有一個例項,只能是不讓外面訪問的類的構造方法,所以就構造方法寫成私有的,向整個系統提供一個例項那麼就只能定義一個static的例項返回回去了,所以程式碼可以這樣寫: public class Sin