Java單例模式機制分析

1、問題引出

單例模式是設計模式中使用比較廣泛的模式之一,它是一種對象建立模式,用於產生一個對象的實例,並能確保系統中一個類只產生一個實例,這樣能夠帶來兩大好處:java

(1)對於頻繁使用的對象實例,能夠省略建立對象實例所花費的時間,這對於那些重量級的對象而言,是一筆很是可觀的系統開銷。設計模式

(2)因爲new操做的次數減小,能夠減輕GC壓力,縮短GC停頓時間。安全

綜上可知,對於系統關鍵組件和被頻繁使用的對象實例,使用單例模式能夠有效的改善系統的性能。性能優化

下面分析單例模式的幾種實現方式:多線程

2、單例模式實現與分析

1. 最簡單的實現

 先看代碼以下:併發

package com.jason.design.singleton;

/**
 * 簡單的單例模式實現,可是不是懶加載形式
 */
public class SingletonClass1 {
    private static final SingletonClass1 instance = new SingletonClass1();
    private SingletonClass1(){}
    public static SingletonClass1 getInstance(){
        return instance;
    }
}

   首先,類的構造函數寫成private的,從而保證別的類不能實例化此類。其次,在類中提供一個靜態的實例instance並提供靜態的getInstance方法給使用者。這樣,使用者就能夠經過這個引用使用到這個類的實例了。函數

2. 性能優化——延遲加載(懶加載lazy loaded)

    上述單例的實現方式很簡單,而且很是可靠。可是有一個問題:不管這個類是否被使用,都會建立一個instance對象。若是這個建立過程很耗時,而且這個類還並不必定會被使用,那麼這個建立過程就是無用的。咱們作延遲加載優化:性能

package com.jason.design.singleton;

/**
 * 延遲加載可是非線程安全的單例模式
 */
public class SingletonClass2 {
    private static SingletonClass2 instance = null;
    private SingletonClass2(){}
    public static SingletonClass2 getInstance(){
        if (instance == null){
            instance = new SingletonClass2();
        }
        return instance;
    }
}

    首先,把instance初始化爲null,確保系統啓動時沒有額外的負載,由於建立過程不在聲明處,因此那個final的修飾必須去掉。優化

    其次,在getInstance()方法中,判斷當前單例是否已經存在,若存在則返回,不存在再創建單例。spa

3. 線程安全問題

    上述代碼在單線程狀況下,沒有什麼問題,但是若是是多線程,會有線程安全問題,在併發狀況下,線程可能各自擁有一個SingletonClass2的對象。

    繼續對其進行優化,首先想到的是加鎖,在getInstance()加上同步鎖,一個線程必須等待另一個線程建立完成後才能使用這個方法,這就保證了單例的惟一性。

package com.jason.design.singleton;

/**
 * 同步獲取單例方法的單例模式,這樣會致使每次獲取單例時都是同步方法,粒度過大
 */
public class SingletonClass3 {
    private static SingletonClass3 instance = null;
    private SingletonClass3(){}
    public static synchronized SingletonClass3 getInstance(){
        if (instance == null){
            instance = new SingletonClass3();
        }
        return instance;
    }
}

4. 性能優化

   上述代碼引入了synchronized鎖,並且是加在整個方法上的,這樣性能可能受到影響。分析一下不難發現出現3中線程安全問題緣由是檢測null的操做和建立對象的操做分離了。若是這兩個操做可以原子地進行就能保證線程安全。因此,能夠將同步塊進行縮小粒度,首先去掉getInstance()的同步操做,而後把同步鎖加載if語句上,修改以下:

package com.jason.design.singleton;
public class SingletonClass4 {

    private static SingletonClass4 instance = null;

    private SingletonClass4() {
    }

    public static SingletonClass4 getInstance() {

        synchronized (SingletonClass4.class) {
            if (instance == null) {
                instance = new SingletonClass4();
            }
        }
        return instance;
    }
}

    繼續分析發現,即便縮小了同步塊的粒度,每次調用getInstance()的時候仍是須要每次都調用同步塊中的內容,性能問題仍是存在。此時可用到Double-check的模式,繼續修改以下:

package com.jason.design.singleton;

/**
 * Double-Check模式的單例模式實現
 */
public class SingletonClass5 {
    private static SingletonClass5 instance = null;
    private SingletonClass5(){}
    private static SingletonClass5 getInstance(){
        if (instance == null) {
            synchronized (SingletonClass5.class) {
                if (instance == null) {
                    instance = new SingletonClass5();
                }
            }
        }
        return instance;
    }
}

    首先判斷instance是否是爲null,若是爲null,加鎖初始化;若是不爲null,直接返回instance。可是這樣就夠了嗎?恰好最近在閱讀Dubbo源碼的時候看到了這種double-check的應用:在Dubbo源碼com/jason/properties/ConfigUtils.java類的時候有這樣一段代碼:

private static volatile Properties PROPERTIES;

public static Properties getProperties() {
    if (PROPERTIES == null) {
        synchronized (ConfigUtils.class) {
            if (PROPERTIES == null) {
                String path = System.getProperty(Constants.DUBBO_PROPERTIES_KEY);
                if (path == null || path.length() == 0) {
                    path = System.getenv(Constants.DUBBO_PROPERTIES_KEY);
                    if (path == null || path.length() == 0) {
                        path = Constants.DEFAULT_DUBBO_PROPERTIES;
                    }
                }
                PROPERTIES = ConfigUtils.loadProperties(path, false, true);
            }
        }
    }
    return PROPERTIES;
}

    對比代碼能夠發現,dubbo中的實現對於PROPERTIES的修飾符多了一個volatile,這個區別在哪裏?

    下面來想一下,建立一個變量須要哪些步驟呢?一個是申請一塊內存,調用構造方法進行初始化操做,另外一個是分配一個指針指向這塊內存。這兩個操做誰在前誰在後呢?JVM規範並無規定。那麼就存在這麼一種狀況,JVM是先開闢出一塊內存,而後把指針指向這塊內存,最後調用構造方法進行初始化。

    下面咱們來考慮這麼一種狀況:線程A開始建立SingletonClass的實例,此時線程B調用了getInstance()方法,首先判斷instance是否爲null。按照咱們上面所說的內存模型,A已經把instance指向了那塊內存,只是尚未調用構造方法,所以B檢測到instance不爲null,因而直接把instance返回了——問題出現了,儘管instance不爲null,但它並無構造完成,就像一套房子已經給了你鑰匙,但你並不能住進去,由於裏面尚未收拾。此時,若是B在A將instance構造完成以前就是用了這個實例,程序就會出現錯誤了!

五、最終實現

(1)同步+Double-check方式:

    在JDK 5以後,Java使用了新的內存模型。volatile關鍵字有了明確的語義——在JDK1.5以前,volatile是個關鍵字,可是並無明確的規定其用途——被volatile修飾的寫變量不能和以前的讀寫代碼調整,讀變量不能和以後的讀寫代碼調整!所以,只要咱們簡單的把instance加上volatile關鍵字就能夠了。

package com.jason.design.singleton;

/**
 * Double-Check模式的單例模式實現,而且加入了volatile的,可保證安全
 */
public class SingletonClass6 {
    private static volatile SingletonClass6 instance = null;
    private SingletonClass6(){}
    private static SingletonClass6 getInstance(){
        if (instance == null) {
            synchronized (SingletonClass6.class) {
                if (instance == null) {
                    instance = new SingletonClass6();
                }
            }
        }
        return instance;
    }
}

(2)靜態內部類實現方式:

然而,這只是JDK1.5以後的Java的解決方案,那以前版本呢?其實,還有另外的一種解決方案,並不會受到Java版本的影響,即:Initialization on Demand Holder(IODH),詳見 http://en.wikipedia.org/wiki/Initialization_on_demand_holder_idiom

package com.jason.design.singleton;
/**
 * 靜態內部類實現的單例模式
 */
public class SingletonClass7 {
    private SingletonClass7(){}
    private static class SingletonInstance{
        private static final SingletonClass7 instance = new SingletonClass7();
    }
    public SingletonClass7 getInstance(){
        return SingletonInstance.instance;
    }
}

    由於SingletonClass7沒有static的屬性,所以在沒有被調用時並不會被初始化,知足延遲加載的優勢,直到調用getInstance()的時候,會首先加載SingletonClassInstance類,這個類有一個static的SingletonClass實例,所以須要調用SingletonClass的構造方法,而後getInstance()將把這個內部類的instance返回給使用者。因爲這個instance是static的,所以並不會構造屢次。

    因爲SingletonClassInstance是私有靜態內部類,因此不會被其餘類知道,一樣,static語義也要求不會有多個實例存在。而且,JSL規範定義,類的構造必須是原子性的,非併發的,所以不須要加同步塊。一樣,因爲這個構造是併發的,因此getInstance()也並不須要加同步。

    綜上:使用內部類的方式實現單例,便可以作到延遲加載,也沒必要使用同步,是一種比較好的實現方式。

(3)單元素枚舉實現

代碼以下:

package com.jason.design.singleton;

/**
 * 枚舉的方式實現單例模式
 */
public enum EnumSingleton {
    INSTANCE;
    private SingletonClass8 instance = null;

    private EnumSingleton() {
        instance = new SingletonClass8();
    }
    public SingletonClass8 getInstance(){
        return instance;
    }
}

單例使用方式:

EnumSingleton.INSTANCE.getInstance();

下面咱們來看看單例是如何被保證的: 
首先,在枚舉中咱們明確了構造方法限制爲私有,在咱們訪問枚舉實例時會執行構造方法,同時每一個枚舉實例都是static final類型的,也就代表只能被實例化一次。在調用構造方法時,咱們的單例被實例化。 
也就是說,由於enum中的實例被保證只會被實例化一次,因此咱們的INSTANCE也被保證明例化一次。 
能夠看到,枚舉實現單例仍是比較簡單的,除此以外咱們再來看一下Enum這個類的聲明:

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable
/**
 * prevent default deserialization
 */
private void readObject(ObjectInputStream in) throws IOException,
    ClassNotFoundException {
    throw new InvalidObjectException("can't deserialize enum");
}
/**
 * Throws CloneNotSupportedException.  This guarantees that enums
 * are never cloned, which is necessary to preserve their "singleton"
 * status.
 *
 * @return (never returns)
 */
protected final Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
}

綜上,單元素枚舉實現單例模式有以下優勢:(1)、自由序列化 (2)、保證只有一個實例(即便使用反射機制也沒法屢次實例化一個枚舉量)(3)、JVM保證的線程安全

六、破壞代理模式

    對象的建立方式有以下四中方式:(1)new (2)克隆(3)序列化(4)反射 在上面實現的方式中,因爲構造函數是private的,所以第一種方式沒法破壞目前咱們實現的單例模式。那麼是否還有能夠經過其餘的三種方式建立對象呢,但其餘三種方式會不會破壞單例模式:

一、克隆可能對單例模式的破壞

    由克隆咱們能夠想到原型模式,原型模式就是經過clone方法實現對象的建立的,clone方式是Object方法,每一個對象都有,那我使用一個單例模式類的對象,調用clone方法,再建立一個新的對象了,那豈不是上面說的單例模式失效了。固然答案是否認,某一個對象直接調用clone方法,會拋出異常,即並不能成功克隆一個對象。調用該方法時,必須實現一個Cloneable 接口。這也就是原型模式的實現方式。還有即若是該類實現了cloneable接口,儘管構造函數是私有的,他也能夠建立一個對象。即clone方法是不會調用構造函數的,他是直接從內存中copy內存區域的。因此單例模式的類是不能夠實現cloneable接口的。

二、序列化可能對單例模式的破壞

    序列化一是能夠實現數據的持久化;二是能夠對象數據的遠程傳輸。 若是過該類implements Serializable,那麼就會在反序列化的過程當中再創一個對象。這個問題的解決辦法就是在反序列化時,指定反序化的對象實例,代碼以下:

package com.jason.design.singleton;

import java.io.Serializable;

/**
 * Double-Check模式的單例模式實現,而且加入了volatile的,可保證安全,可被串行化的單例
 */
public class SingletonClass8 implements Serializable {

    private static volatile SingletonClass8 instance = null;
    private SingletonClass8(){}
    private static SingletonClass8 getInstance(){
        if (instance == null) {
            synchronized (SingletonClass8.class) {
                if (instance == null) {
                    instance = new SingletonClass8();
                }
            }
        }
        return instance;
    }

    /**
     * 在反序列化時,指定反序化的對象實例
     * @return
     */
    private Object readResolve(){
        return instance;
    }
}

3.反射可能對單例模式的破壞

    反射是能夠獲取類的構造函數,再加一行 setAccessible(true);就能夠調用私有的構造函數,建立對象了。那麼防止反射破壞Java單例模式的方法就是:當第二次調用構造函數時拋出異常。代碼以下:

package com.jason.design.singleton;

/**
 * 靜態內部類實現的單例模式,防止反射機制破壞單例
 */
public class SingletonClass10 {
    private static volatile boolean flag = true;
    private SingletonClass10(){
        if (flag){
            flag = false;
        }else {
            throw new RuntimeException();
        }
    }
    private static class SingletonInstance{
        private static final SingletonClass10 instance = new SingletonClass10();
    }
    public SingletonClass10 getInstance(){
        return SingletonInstance.instance;
    }
}

 

參考文獻:http://www.jb51.net/article/80201.htm

http://blog.51cto.com/devbean/203501

http://blog.csdn.net/yy254117440/article/details/52305175

http://blog.csdn.net/chao_19/article/details/51112962

相關文章
相關標籤/搜索