設計模式(一)——設計模式概述中簡單介紹了設計模式以及各種設計模式的基本概念,本文主要介紹 單例設計模式
。包括單例的概念、用途、實作方式、如何防止被序列化破壞等。
概念
單例模式(
Singleton Pattern
)是 Java 中最簡單的設計模式之一。這種類型的設計模式屬于建立型模式。在 GOF 書中給出的定義為:保證一個類僅有一個執行個體,并提供一個通路它的全局通路點。
單例模式一般展現在類聲明中,單例的類負責建立自己的對象,同時確定隻有單個對象被建立。這個類提供了一種通路其唯一的對象的方式,可以直接通路,不需要執行個體化該類的對象。
用途
單例模式有以下兩個優點:
在記憶體裡隻有一個執行個體,減少了記憶體的開銷,尤其是頻繁的建立和銷毀執行個體(比如網站首頁頁面緩存)。
避免對資源的多重占用(比如寫檔案操作)。
有時候,我們在選擇使用單例模式的時候,不僅僅考慮到其帶來的優點,還有可能是有些場景就必須要單例。比如類似”一個黨隻能有一個主席”的情況。
實作方式
我們知道,一個類的對象的産生是由類構造函數來完成的。如果一個類對外提供了
public
的構造方法,那麼外界就可以任意建立該類的對象。是以,如果想限制對象的産生,一個辦法就是将構造函數變為私有的(至少是受保護的),使外面的類不能通過引用來産生對象。同時為了保證類的可用性,就必須提供一個自己的對象以及通路這個對象的靜态方法。

餓漢式
下面是一個簡單的單例的實作:
//code 1
public class Singleton {
//在類内部執行個體化一個執行個體
private static Singleton instance = new Singleton();
//私有的構造函數,外部無法通路
private Singleton() {
}
//對外提供擷取執行個體的靜态方法
public static Singleton getInstance() {
return instance;
}
}
使用以下代碼測試:
//code2
public class SingletonClient {
public static void main(String[] args) {
SimpleSingleton simpleSingleton1 = SimpleSingleton.getInstance();
SimpleSingleton simpleSingleton2 = SimpleSingleton.getInstance();
System.out.println(simpleSingleton1==simpleSingleton2);
}
}
輸出結果:
true
code 1就是一個簡單的單例的實作,這種實作方式我們稱之為餓漢式。所謂餓漢。這是個比較形象的比喻。對于一個餓漢來說,他希望他想要用到這個執行個體的時候就能夠立即拿到,而不需要任何等待時間。是以,通過
static
的靜态初始化方式,在該類第一次被加載的時候,就有一個
SimpleSingleton
的執行個體被建立出來了。這樣就保證在第一次想要使用該對象時,他已經被初始化好了。
同時,由于該執行個體在類被加載的時候就建立出來了,是以也避免了線程安全問題。(原因見:在深度分析Java的ClassLoader機制(源碼級别)、Java類的加載、連結和初始化)
還有一種餓漢模式的變種:
//code 3
public class Singleton2 {
//在類内部定義
private static Singleton2 instance;
static {
//執行個體化該執行個體
instance = new Singleton2();
}
//私有的構造函數,外部無法通路
private Singleton2() {
}
//對外提供擷取執行個體的靜态方法
public static Singleton2 getInstance() {
return instance;
}
}
code 3和code 1其實是一樣的,都是在類被加載的時候執行個體化一個對象。
餓漢式單例,在類被加載的時候對象就會執行個體化。這也許會造成不必要的消耗,因為有可能這個執行個體根本就不會被用到。而且,如果這個類被多次加載的話也會造成多次執行個體化。其實解決這個問題的方式有很多,下面提供兩種解決方式,第一種是使用靜态内部類的形式。第二種是使用懶漢式。
靜态内部類式
先來看通過靜态内部類的方式解決上面的問題:
//code 4
public class StaticInnerClassSingleton {
//在靜态内部類中初始化執行個體對象
private static class SingletonHolder {
private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
//私有的構造方法
private StaticInnerClassSingleton() {
}
//對外提供擷取執行個體的靜态方法
public static final StaticInnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
這種方式同樣利用了classloder的機制來保證初始化
instance
時隻有一個線程,它跟餓漢式不同的是(很細微的差别):餓漢式是隻要
Singleton
類被裝載了,那麼
instance
就會被執行個體化(沒有達到lazy loading效果),而這種方式是
Singleton
類被裝載了,
instance
不一定被初始化。因為
SingletonHolder
類沒有被主動使用,隻有顯示通過調用
getInstance
方法時,才會顯示裝載
SingletonHolder
類,進而執行個體化
instance
。想象一下,如果執行個體化
instance
很消耗資源,我想讓他延遲加載,另外一方面,我不希望在
Singleton
類加載時就執行個體化,因為我不能確定
Singleton
類還可能在其他的地方被主動使用進而被加載,那麼這個時候執行個體化
instance
顯然是不合适的。這個時候,這種方式相比餓漢式更加合理。
懶漢式
下面看另外一種在該對象真正被使用的時候才會執行個體化的單例模式——懶漢模式。
//code 5
public class Singleton {
//定義執行個體
private static Singleton instance;
//私有構造方法
private Singleton(){}
//對外提供擷取執行個體的靜态方法
public static Singleton getInstance() {
//在對象被使用的時候才執行個體化
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上面這種單例叫做懶漢式單例。懶漢,就是不會提前把執行個體建立出來,将類對自己的執行個體化延遲到第一次被引用的時候。
getInstance
方法的作用是希望該對象在第一次被使用的時候被
new
出來。
有沒有發現,其實code 5這種懶漢式單例其實還存在一個問題,那就是線程安全問題。在多線程情況下,有可能兩個線程同時進入
if
語句中,這樣,在兩個線程都從if中退出的時候就建立了兩個不一樣的對象。(這裡就不詳細講解了,不了解的請惡補多線程知識)。
線程安全的懶漢式
針對線程不安全的懶漢式的單例,其實解決方式很簡單,就是給建立對象的步驟加鎖:
//code 6
public class SynchronizedSingleton {
//定義執行個體
private static SynchronizedSingleton instance;
//私有構造方法
private SynchronizedSingleton(){}
//對外提供擷取執行個體的靜态方法,對該方法加鎖
public static synchronized SynchronizedSingleton getInstance() {
//在對象被使用的時候才執行個體化
if (instance == null) {
instance = new SynchronizedSingleton();
}
return instance;
}
}
這種寫法能夠在多線程中很好的工作,而且看起來它也具備很好的延遲加載,但是,遺憾的是,他效率很低,因為99%情況下不需要同步。(因為上面的
synchronized
的加鎖範圍是整個方法,該方法的所有操作都是同步進行的,但是對于非第一次建立對象的情況,也就是沒有進入
if
語句中的情況,根本不需要同步操作,可以直接傳回
instance
。)
雙重校驗鎖
針對上面code 6存在的問題,相信對并發程式設計了解的同學都知道如何解決。其實上面的代碼存在的問題主要是鎖的範圍太大了。隻要縮小鎖的範圍就可以了。那麼如何縮小鎖的範圍呢?相比于同步方法,同步代碼塊的加鎖範圍更小。code 6可以改造成:
//code 7
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
code 7是對于code 6的一種改進寫法,通過使用同步代碼塊的方式減小了鎖的範圍。這樣可以大大提高效率。(對于已經存在
singleton
的情況,無須同步,直接return)。
但是,事情這的有這麼容易嗎?上面的代碼看上去好像是沒有任何問題。實作了惰性初始化,解決了同步問題,還減小了鎖的範圍,提高了效率。但是,該代碼還存在隐患。隐患的原因主要和Java記憶體模型(JMM)有關。考慮下面的事件序列:
線程A發現變量沒有被初始化, 然後它擷取鎖并開始變量的初始化。
由于某些程式設計語言的語義,編譯器生成的代碼允許線上程A執行完變量的初始化之前,更新變量并将其指向部分初始化的對象。
線程B發現共享變量已經被初始化,并傳回變量。由于線程B确信變量已被初始化,它沒有擷取鎖。如果在A完成初始化之前共享變量對B可見(這是由于A沒有完成初始化或者因為一些初始化的值還沒有穿過B使用的記憶體(緩存一緻性)),程式很可能會崩潰。
(上面的例子不太能了解的同學,請惡補JAVA記憶體模型相關知識)
在J2SE 1.4或更早的版本中使用雙重檢查鎖有潛在的危險,有時會正常工作(區分正确實作和有小問題的實作是很困難的。取決于編譯器,線程的排程和其他并發系統活動,不正确的實作雙重檢查鎖導緻的異常結果可能會間歇性出現。重制異常是十分困難的。) 在J2SE 5.0中,這一問題被修正了。volatile關鍵字保證多個線程可以正确處理單件執行個體
是以,針對code 7 ,可以有code 8 和code 9兩種替代方案:
使用
volatile
//code 8
public class VolatileSingleton {
private static volatile VolatileSingleton singleton;
private VolatileSingleton() {
}
public static VolatileSingleton getSingleton() {
if (singleton == null) {
synchronized (VolatileSingleton.class) {
if (singleton == null) {
singleton = new VolatileSingleton();
}
}
}
return singleton;
}
}
上面這種雙重校驗鎖的方式用的比較廣泛,他解決了前面提到的所有問題。但是,即使是這種看上去完美無缺的方式也可能存在問題,那就是遇到序列化的時候。詳細内容後文介紹。
final
//code 9
class FinalWrapper<T> {
public final T value;
public FinalWrapper(T value) {
this.value = value;
}
}
public class FinalSingleton {
private FinalWrapper<FinalSingleton> helperWrapper = null;
public FinalSingleton getHelper() {
FinalWrapper<FinalSingleton> wrapper = helperWrapper;
if (wrapper == null) {
synchronized (this) {
if (helperWrapper == null) {
helperWrapper = new FinalWrapper<FinalSingleton>(new FinalSingleton());
}
wrapper = helperWrapper;