設計模式-深入單例模式精髓-剖析單例模式適用場景以及多線程問題

設計模式最常見的模式之一單例模式,廢話不多說,前面的文章已經有對設計模式的7大原則有過介紹,從本文開始對每一種設計模式以及設計模式所適用的場景做全面的剖析。

本文是針對常見的設計模式之一單例模式做一個分析,單例模式有懶漢模式、餓漢模式、雙重鎖模式、靜態內部類模式,接下來一一呈現並對單例模式下多線程需要注意的地方做出分析和解決。

概念

單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。

定義:Ensure a class has only one instance,and provide a global point of access to it.(確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例。)

這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。

實現單例模式的思路是:一個類能返回對象一個引用(永遠是同一個)和一個獲得該實例的方法(必須是靜態方法,通常使用getInstance這個名稱);當我們調用這個方法時,如果類持有的引用不爲空就返回這個引用,如果類保持的引用爲空就創建該類的實例並將實例的引用賦予該類保持的引用;同時我們還將該類的構造函數定義爲私有方法,這樣其他處的代碼就無法通過調用該類的構造函數來實例化該類的對象,只有通過該類提供的靜態方法來得到該類的唯一實例。基本實現方式如下:

public class Singleton {
     private static Singleton singleton;        
     //限制產生多個對象
     private Singleton(){
     }
     //通過該方法獲得實例對象
     public static Singleton getSingleton(){
             if(singleton==null)singleton = new Singleton();
             return singleton;
     }  
     //類中其他方法,儘量是static
     public static void doSomething(){
     }
}

懶漢模式

我們可以看到上文中的代碼實現,在獲取實例的時候去實例化一個對象,這種實現方式就叫做懶漢模式。

優點:只有當第一次使用的時候纔會創建,有利於減少系統啓動時間;

缺點:在第一次使用的時候需要佔用創建時間;

接下來我們思考一下,如果有兩個線程同時調用上面getSingleton()方法,可以能會出現什麼問題?

有可能第一個線程獲取的對象實例和第二個線程獲取的實例不一致,我們可以將代碼修改成如下:

public class Singleton {
     private static Singleton singleton;        
     //限制產生多個對象
     private Singleton(){
     }
     //通過該方法獲得實例對象
     public static synchronized Singleton getSingleton(){
             if(singleton==null)singleton = new Singleton();
             return singleton;
     }  
     //類中其他方法,儘量是static
     public static void doSomething(){
     }
}

在getSingleton()方法前增進同步標識synchronized,synchronized增加的是一個互斥鎖雖然可以保障實例的唯一性,但是也降低了多線程下的執行效率。怎麼解決互斥鎖導致的性能問題?

雙重鎖懶漢模式(Double Check Lock)

我們先看下面代碼的實現方式:

public class Singleton {
     private volatile static Singleton singleton;        
     //限制產生多個對象
     private Singleton(){
     }
     //通過該方法獲得實例對象
     public static Singleton getSingleton(){
             if(singleton==null){
                synchronized(Singleton.class){
                  if(singleton == null)singleton = new Singleton();
                }
             }
             return singleton;
     }  
     //類中其他方法,儘量是static
     public static void doSomething(){
     }
}

 這種方式優化了普通懶漢模式的性能,接下來我們分析一下雙重鎖(DCL)怎麼帶來的性能提升以及爲什麼要有多重判斷。

  • 第一次判斷singleton是否爲null

  第一次判斷是在Synchronized同步代碼塊外進行判斷,由於單例模式只會創建一個實例,並通過getInstance方法返回singleton對象,所以,第一次判斷,是爲了在singleton對象已經創建的情況下,避免進入同步代碼塊,提升效率。

  • 第二次判斷singleton是否爲null

  第二次判斷是爲了避免以下情況的發生。
  (1)假設:線程A已經經過第一次判斷,判斷singleton=null,準備進入同步代碼塊.
  (2)此時線程B獲得時間片,猶豫線程A並沒有創建實例,所以,判斷singleton仍然=null,所以線程B創建了實例singleton。
  (3)此時,線程A再次獲得時間片,猶豫剛剛經過第一次判斷singleton=null(不會重複判斷),進入同步代碼塊,這個時候,我們如果不加入第二次判斷的話,那麼線程A又會創造一個實例singleton,就不滿足我們的單例模式的要求,所以第二次判斷是很有必要的。

  • 爲什麼要加Volatile關鍵字

  其實,上面兩點比較好理解,第三點,既然有了Synchronized作爲限制,爲什麼還要加入Volatile呢?

  首先,我們需要知道Volatile可以保證可見性和原子性,同時保證JVM對指令不會進行重排序。
  其次,這點也很關鍵,對象的創建不是一步完成的,是一個符合操作,需要3個指令。
  我們結合這一句代碼來解釋:

singleton = new Singleton();  
  1. 指令1:獲取singleton對象的內存地址
  2. 指令2:初始化singleton對象
  3. 指令3:將這塊內存地址,指向引用變量singleton。

  那麼,這樣我們就比較好理解,爲什麼要加入Volatile變量了。由於Volatile禁止JVM對指令進行重排序。所以創建對象的過程仍然會按照指令1-2-3的有序執行。
  反之,如果沒有Volatile關鍵字,假設線程A正常創建一個實例,那麼指定執行的順序可能2-1-3,當執行到指令1的時候,線程B執行getInstance方法,獲取到的,可能是對象的一部分,或者是不正確的對象,程序可能就會報異常信息。

      然而這種方式並未完全解決,鎖帶來的性能問題,因此餓漢模式出現了。

餓漢模式

餓漢模式指在類中直接定義全局的靜態對象的實例並初始化,然後提供一個方法獲取該實例對象。懶漢模式和餓漢模式的最大不同在於,懶漢模式在類中定義了單例但是並未實例化,實例化的過程是在獲取單例對象的方法中實現的,也就是說,在第一次調用懶漢模式時,該對象一定爲空,然後去實例化對象並賦值,這樣下次就能直接獲取對象了;而餓漢模式是在定義單例對象的同時將其實例化的,直接使用便可。也就是說,在餓漢模式下,在Class Loader完成後該類的實例便已經存在於JVM中了,代碼如下:

public class Singleton {
     private static Singleton singleton = new Singleton();        
     //限制產生多個對象
     private Singleton(){
     }
     //通過該方法獲得實例對象
     public static Singleton getSingleton(){
             return singleton;
     }  
     //類中其他方法,儘量是static
     public static void doSomething(){
     }
}

瞭解完餓漢模式的實現方式後,互斥鎖不存在了,性能問題也就得到了解決。其主要問題在於增加了啓動所需要的時間和內存。

餓漢模式:以空間換時間,懶漢模式:以時間換空間,是否有種方式可以解決所有問題呢?

靜態內部類模式

靜態內部類實現代碼如下:

public class SingleTon{
  private SingleTon(){}
 
  private static class SingleTonHoler{
     private static SingleTon INSTANCE = new SingleTon();
 }
 
  public static SingleTon getInstance(){
    return SingleTonHoler.INSTANCE;
  }
}

靜態內部類的優點是:外部類加載時並不需要立即加載內部類,內部類不被加載則不去初始化INSTANCE,故而不佔內存。即當SingleTon第一次被加載時,並不需要去加載SingleTonHoler,只有當getInstance()方法第一次被調用時,纔會去初始化INSTANCE,第一次調用getInstance()方法會導致虛擬機加載SingleTonHoler類,這種方法不僅能確保線程安全,也能保證單例的唯一性,同時也延遲了單例的實例化。

場景與實踐

單例模式是23個模式中比較簡單的模式,應用也非常廣泛,如在Spring中,每個Bean默認就是單例的,這樣做的優點是Spring容器可以管理這些Bean的生命期,決定什麼時候創建出來,什麼時候銷燬,銷燬的時候要如何處理,等等。如果採用非單例模式(Prototype類型),則Bean初始化後的管理交由J2EE容器,Spring容器不再跟蹤管理Bean的生命週期。
使用單例模式需要注意的一點就是JVM的垃圾回收機制,如果我們的一個單例對象在內存中長久不使用,JVM就認爲這個對象是一個垃圾,在CPU資源空閒的情況下該對象會被清理掉,下次再調用時就需要重新產生一個對象。如果我們在應用中使用單例類作爲有狀態值(如計數器)的管理,則會出現恢復原狀的情況,應用就會出現故障。如果確實需要採用單例模式來記錄有狀態的值,有兩種辦法可以解決該問題:

  • 由容器管理單例的生命週期

Java EE容器或者框架級容器(如Spring)可以讓對象長久駐留內存。當然,自行通過管理對象的生命期也是一個可行的辦法,既然有那麼多的工具提供給我們,爲什麼不用呢?

  • 狀態隨時記錄

可以使用異步記錄的方式,或者使用觀察者模式,記錄狀態的變化,寫入文件或寫入數據庫中,確保即使單例對象重新初始化也可以從資源環境獲得銷燬前的數據,避免應用數據丟失。

筆者的微信公衆號,每天一篇好文章:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章