Java 單例模式


主要參考自 菜鳥教程

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

注意:

  • 單例類只能有一個實例
  • 單例類必須自己創建自己的唯一實例
  • 單例類必須給所有其他對象提供這一實例

介紹

意圖:保證一個類就有一個實例,并提供一個訪問它的全局訪問點。
主要解決:一個全局使用的類頻繁地創建與銷毀。
何時使用:想控制實例數目,節省系統資源。
優點: 1.內存里只有一個實例,減少內存開銷,尤其是頻繁創建和銷毀。2. 避免對資源的多重利用。
缺點:沒接口,不能繼承,與單一職責原則沖突,一個類應該只關心內部邏輯,而不關心外面怎樣來實例化。
使用場景: 1.要求生產唯一序列號。2.WEB中的計數器,不用每次刷新都在數據庫里加一次,用單例先緩存起來。3.創建一個對象需要消耗的資源過多。

單例模式實現

懶漢模式(線程不安全)

public class Singleton {

    private static Singleton instance;

    private Singleton() {
    }

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

}

? ? ? ?這是一種最基本的單例模式的示例,這是一種線程不安全的模式,在并發環境中很可能會出現很多個Singleton實例。

懶漢模式(線程安全)

public class Singleton {

    private static Singleton instance;

    private Singleton() {
    }

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

}

? ? ? ?相對于上面那種,這種方式能夠在多線程中很好地工作,采用了synchronized方法重量鎖,同時在絕大多數情況下,都是不需要同步的。

懶漢模式(雙重校驗)

public class Singleton {

    private static volatile Singleton instance;

    private Singleton() {
    }

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

? ? ? ?相對于上面的方法來說,這一種方法的效率極大的提升了。在實際的使用中,new創建的情況是很少的,絕大部分都是可以并行的讀操作,執行效率可以大大提高。不過,這里有一點問題,可能讀者也注意到了在instance起那么多了一個修飾符volatile,實際上這個修飾符起了很關鍵的作用,這一部分的內容將會在后面詳細講述。

餓漢模式

public class Singleton {

    private static volatile Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }

}

? ? ? ?這是一種基于ClassLoader的方式,將在初始化階段通過<clinit>對instance賦值(這一部分可以參見我的上一篇文章JVM類加載機制),避免了多線程的同步問題。雖然大部分時間Singleton類裝載都是發生在調用static方法時發生的,但是我們不能夠保證沒有其他的方式可以完成觸發類的初始化(比如:通過反射,當然在反射的情況下又會有新的問題,這一部分內容也在最后講述),在這種情況下,并沒有實現延遲加載的效果。

靜態內部類模式

public class Singleton {

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    private Singleton() {
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

}

? ? ? ?這種方式能夠達到雙檢鎖方式一樣的功效,但實現更簡單。在這種方式中,INSTANCE并不會隨著Singleton的裝載而被實例化,只有在使用了getInstance()的條件下才會被創建,這樣實現了lazy-loading的效果。

枚舉方式

public enum Singleton {
    
    INSTANCE;

    public void something() {

    }
}

? ? ? ?使用枚舉的方式實現單例不僅能夠避免多線程同步問題,而且還自動支持序列化機制,防止反序列化重新創建新的對象,絕對防止多次實例化,同時還不能通過reflection attack來調用私有構造方法。可能是現在最佳的方法,這種方法的具體內涵將后半部分進行簡單的介紹。

相關知識

volatile關鍵字

? ? ? ?首先,先談一下什么是原子操作。所謂原子操作是指不會被線程調度機制打斷的操作;這種操作一旦開始,就一直運行到結束,中間不會有任何線程切換。
? ? ? ?比如,一個賦值操作就是一個原子操作:

i = 5;

? ? ? ?在Java中的原子操作只有8種,lock、unlock、read、load、use、assign、store以及write,看到這里大部分人都會認為賦值操作是原子性的,但是我們并不能夠下這樣的定論,因為在32為JVM中long和double的賦值操作不是原子性的,這也是在并發中這兩個基本數據類型經常會遇到的數據撕裂情況。
? ? ? ?從上面的情況我們可以知道,聲明并且賦值并不會是一個原子操作:

int i = 0;

? ? ? ?上述這一條語句至少包括兩個操作:

  1. 聲明一個變量i
  2. 對i進行賦值

? ? ? ?很顯然,這會產生一個中間態:變量i已經聲明但是并且賦值的情況,這對于多線程環境就十分致命了。
? ? ? ?我們再簡單了解下指令重排,指令重排顧名思義,就是JVM為了優化指令,提高程序運行效率,在不影響單線程程序執行結果前提下進行指令順序的調整。
? ? ? ?假設我們的雙重校驗方式并未采取volatile關鍵字修飾,那么:

    instance = new Singleton();

? ? ? ?這一條很簡單的賦值語句,實際上在JVM內部已經轉換為了至少三條指令:

        //1:分配對象的內存空間
        memory = allocate();
        //2:初始化對象
        ctorInstance(memory);
        //3:設置instance指向剛分配的內存地址
        instance = memory;

? ? ? ?但是在JVM的即時編譯器(運行期優化)中存在指令重排的優化操作。也就是說,上面的2步驟和3步驟的執行順序并不能夠保證。那么,很有可能發生:

        //1:分配對象的內存空間
        memory = allocate();
        //3:設置instance指向剛分配的內存地址,此時對象還沒被初始化
        instance = memory;
        //2:初始化對象
        ctorInstance(memory); 

? ? ? ?在這種情況下,instance指向分配好的內存放在的前面,而這段內存的初始化卻放在了后面,這樣就意味著:在線程A初始化完成這段內存之前,線程B再同步代碼之前就會發現instance不為空,此時線程B就會獲得instance對象進行使用這就可能發生一定的錯誤。
? ? ? ?這種情況下,volatile關鍵字就起到了它的作用。volatile關鍵字除了大家經常使用的保持內存可見性的功能之外,使用volatile關鍵字修飾的變量禁止指令重排序。
? ? ? ?volatile關鍵字通過內存屏障這一功能來防止指令被重新排序。為了實現這一功能,編譯器再生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器排序(關于內存屏障的具體內容可以瀏覽并發編程網)。

通過反射破壞單例模式

? ? ? ?在Java中可以創建一個對象的方式,大概有四種:new、克隆、反射、序列化。
? ? ? ?首先,在單例模式中肯定是無法通過最基本的new來創建對象,所以對于這種我們不予考慮了。其次,克隆是通過Cloneable接口實現對象的創建,即時構造函數是私有的,通過這種方式我們也可以創建一個對象(克隆直接從內存中賦值內存區域)。不過正如前文所言,只要我們不實現克隆,那么就不可能通過克隆模式創建對象(在單例模式中并沒有實現Cloneable接口的必要)。
? ? ? ?反射是Java中比較神奇的一個功能,Java中的反射技術可以獲取類所有的方法、成員變量還能夠訪問私有構造方法,這樣一來就會破壞單例模式的結構,我們先用雙重驗證測試一下:

public class Singleton {

    private static volatile Singleton instance;

    private Singleton() {
    }

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

    public static void main(String[] args) {
        try {
            Constructor constructor = Singleton.class.getDeclaredConstructor();
            constructor.setAccessible(true);
            Singleton singleton1 = (Singleton) constructor.newInstance();
            Singleton singleton2 = (Singleton) constructor.newInstance();
            Singleton singleton3 = Singleton.getInstance();
            Singleton singleton4 = Singleton.getInstance();
            System.out.println(singleton1);
            System.out.println(singleton2);
            System.out.println(singleton3);
            System.out.println(singleton4);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
}

輸出結果為:

Singleton@1540e19d
Singleton@677327b6
Singleton@14ae5a5
Singleton@14ae5a5

? ? ? ?很顯然,通過反射技術生成的兩個實例不同,通過常規方法獲取的兩個實例相同。對于這種情況,我們也有一定的解決方案,我們可以重新定義構造函數,當構造函數第二次調用是拋出異常:

    private Singleton() throws Exception {
        if (null != instance) {
            throw new Exception("duplicate instance create error!" + Singleton.class.getName());
        }
    }
    

通過序列化破壞單例模式

? ? ? ?序列化是指將對象的狀態信息轉換為可以存儲或傳輸的形式的過程。在序列化期間,對象將其當前狀態寫入臨時或持久性存儲區。以后可以通過從存儲區讀取或反序列化對象的狀態,重新創建該對象
? ? ? ?相信大家都注意到了重新創建該對象這幾個字,只要單例模式類實現了Serializable或者Externalizable接口,那么就會在反序列化的過程中在創建一個對象,這樣可能在整個單例模式的生命周期中出現兩個不同的實例,這是我們所不能允許的。對于這種情況也有一種簡單的處理辦法,通過重寫readResolve函數我們可以避免這一情況,那么我們現在的雙重檢測模式應該是:

public class Singleton implements Serializable {

    private static volatile Singleton instance;

    private Singleton() throws Exception {
        if (null != instance) {
            throw new Exception("duplicate instance create error!" + Singleton.class.getName());
        }
    }

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

    private Object readResolve() throws ObjectStreamException {
        return instance;
    }
}

? ? ? ? 其實無論使用哪種接口,當從I/O中讀取對象是,都會調用readResolve方法(實際上,在反序列化過程中,就是使用readResolve方法返回的對象替代直接在序列化過程中創建的對象)。

枚舉類型單例模式[1]

? ? ? ?通過枚舉類型實現一個單例模式是被很多文章推薦的,這種方式可以很簡單的避免了前面我們研究探討的問題,但對于這其中的原理可能并不是很清楚,下面我們從一個Enum類型的實現上開始討論這個問題,首先我們先定義一個枚舉類:

public enum Expression {
    INSIDIOUS, FUNNY, SPRAY
}

? ? ? ?這是一個很簡單的枚舉類,但是實際上這一部分代碼在編譯以后的內容卻是大不相同的。我們使用反編譯工具JAD進行反編譯,反編譯的結果為:

public final class Expression extends Enum
{

    public static Expression[] values()
    {
        return (Expression[])$VALUES.clone();
    }

    public static Expression valueOf(String name)
    {
        return (Expression)Enum.valueOf(Expression, name);
    }

    private Expression(String s, int i)
    {
        super(s, i);
    }

    public static final Expression INSIDIOUS;
    public static final Expression FUNNY;
    public static final Expression SPRAY;
    private static final Expression $VALUES[];

    static 
    {
        INSIDIOUS = new Expression("INSIDIOUS", 0);
        FUNNY = new Expression("FUNNY", 1);
        SPRAY = new Expression("SPRAY", 2);
        $VALUES = (new Expression[] {
            INSIDIOUS, FUNNY, SPRAY
        });
    }
}

? ? ? ?反編譯顯示的結果遠遠比我們想象的復雜得多。通過閱讀反編譯的代碼我們可以很清晰的認識到實際上枚舉類型是通過常見一個繼承自Enum的類來完成的(至于這部分代碼比較簡單,在這里就不再敘述)。
? ? ? ?從線程安全的角度上說,枚舉類型采用ClassLoader的方式來創建一個實例,所以這一過程一定是線程安全的。
? ? ? ?對于枚舉類型,在序列化過程中Java僅僅是將枚舉對象的name屬性出書到結果中,在反序列化的規則則是通過valueOf方法來根據名字查找枚舉對象。同時,編譯器不允許對Enum的序列化機制進行定制,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。簡單了解下valueOf源代碼:

public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {  
            T result = enumType.enumConstantDirectory().get(name);  
            if (result != null)  
                return result;  
            if (name == null)  
                throw new NullPointerException("Name is null");  
            throw new IllegalArgumentException(  
                "No enum const " + enumType +"." + name);  
        }  

? ? ? ?代碼會嘗試從EnumType這個Class對象的enumConstantDirectory方法中獲取map中名為name的枚舉對象,如果不存在則拋出異常。如果跟蹤enumConstantDirectory方法我們則會發現這一過程是通過反射的方式調用完成的,所以我們可以認為枚舉類型在序列化上是存在著保障的。
? ? ? ?此方法在反射上也應該是具有保障的,不過由于本人對反射了解并不夠深入,所以并不是很了解這一過程,準備在以后將這一部分填充。

總結

? ? ? ?這篇文章相對于其他相似的文章來說,更希望能夠提供更加詳細的講解,而不是就把這幾種方式簡單的羅列而已,不過由于本人對JVM的各種機制還處于一種簡單了解的狀況,所以還有所局限。如果您發現了本篇文章中存在的問題,請您跟我聯系。


  1. http://www.hollischuang.com/archives/197 ?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 目錄一.什么是單例?二.有幾種?三.應用場景四.注意的地方 一.什么是單例? 單例模式 保證一個類在內存中只有一個...
    在挖坑的猿閱讀 874評論 0 0
  • 單例模式(SingletonPattern)一般被認為是最簡單、最易理解的設計模式,也因為它的簡潔易懂,是項目中最...
    成熱了閱讀 4,293評論 4 34
  • 一、前言 作為對象的創建模式,單例模式確保某一個類只有一個實例,而且自行實例化并向整個系統提供這個實例。這個類稱為...
    manimaniho閱讀 449評論 0 0
  • 一級標題 二級標題 三級標題 六級標題 列表 無序列表 1 2 3 有序列表 1 2 3 這里是引用的內容引用內引...
    FANERARTIST閱讀 228評論 0 1
  • 孩子考入高中后,離校距離較遠,來回倒車費事費力,因此買車提到議事議程。但家里沒人懂車,有空了,自己就是在網...
    鲇魚200276閱讀 39評論 0 0