單例設(shè)計模式

1. 基本概念

java 進程 內(nèi)存中只有一個 對象實例。

  • 實現(xiàn)的基本原則:
  1. 構(gòu)造器私有化,不允許外部創(chuàng)建對象。
  2. 提供public static 的訪問點,返回創(chuàng)建的對象。
  • 應(yīng)用場景:
  1. spring ioc 容器默認單例。
  2. 全局配置對象,全局一份
  3. 框架封裝,結(jié)合其他設(shè)計模式一起使用。
2. 實現(xiàn)方式
    1. 餓漢式
    1. 懶漢式

雙重檢查鎖
靜態(tài)內(nèi)部類

  • 3.ThreadLocal
    1. 枚舉
    1. CSA 原子類
    1. 注冊式單例
3. 破壞單例的方式
    1. 暴力反射
    1. 序列化和反序列化
4.代碼實現(xiàn)

上面我們簡單的總結(jié)了一下有關(guān)單例模式的相關(guān)知識點,接下來我們就來用代碼實現(xiàn)一下常見單例的幾種寫法。以及如何保證單例的線程安全。

4.1 餓漢式

public class HungarySingleton {

    //類加載時進行初始化
    private static final  HungarySingleton instance = new HungarySingleton();
    
    // 構(gòu)造器初始化
    private HungarySingleton() {}

    // 全局訪問點
    public static HungarySingleton getInstance() {
        return instance;
    }
}

優(yōu)點:餓漢式在類加載時就初始對象,并且只初始化一次,所以是線程安全的。
缺點:這種寫法是強引用,在JVM 里面永遠不會被回收。同時在jvm 啟動時也會消耗一定的資源,不管是否使用,都已經(jīng)創(chuàng)建了,存在資源的浪費,如果在jvm 里面餓漢式單例太多了,就很浪費資源了,并且被創(chuàng)建的對象也無法被垃圾回收。后面我們會講懶加載,這里我們首先來測試一下單例破壞的反射和序列化。

  • 反射破壞:
 /**
     * 反射破壞
     * @throws Exception
     */
    public static void test2() throws Exception {
        //得到默認構(gòu)造器
        Constructor<HungarySingleton> declaredConstructor = HungarySingleton.class.getDeclaredConstructor();
        //強制訪問
        declaredConstructor.setAccessible(true);
        // 創(chuàng)建對象
        HungarySingleton hungarySingleton = declaredConstructor.newInstance();
        System.out.println(hungarySingleton);
        HungarySingleton instance = HungarySingleton.getInstance();
        System.out.println(instance);
    }
  • 測試結(jié)果
com.example.designpattern.singleton.HungarySingleton@7440e464
com.example.designpattern.singleton.HungarySingleton@49476842

Process finished with exit code 0

從上面的測試結(jié)果看出來,餓漢式創(chuàng)建的單例可以被發(fā)射破壞。為了解決這個問題我們可以 在構(gòu)造器那里做一下手腳:因為是反射調(diào)用構(gòu)造器,所以我們可以在構(gòu)造器中判斷一下,如果對象已經(jīng)存在了就拋出異常,防止再創(chuàng)建一次對象。

  • 修改構(gòu)造器:
public class HungarySingleton {

    //類加載時進行初始化
    private static final  HungarySingleton instance = new HungarySingleton();

    // 構(gòu)造器初始化
    private HungarySingleton() {
        if (instance!=null) {//判斷對象是否已經(jīng)被創(chuàng)建
            throw new RuntimeException("請不要重復(fù)創(chuàng)建對象");
        }
    }

    // 全局訪問點
    public static HungarySingleton getInstance() {
        return instance;
    }
}
  • 測試結(jié)果:
Exception in thread "main" java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at com.example.designpattern.singleton.HungarySingleton.test2(HungarySingleton.java:62)
    at com.example.designpattern.singleton.HungarySingleton.main(HungarySingleton.java:48)
Caused by: java.lang.RuntimeException: 請不要重復(fù)創(chuàng)建對象
    at com.example.designpattern.singleton.HungarySingleton.<init>(HungarySingleton.java:22)
    ... 6 more

Process finished with exit code 1

從上面的測試結(jié)果我們可以看出,反射是可以破壞單例的,當(dāng)然針對餓漢式單例的反射破壞我們也可以有一些措施。接下來我們來看看 序列化和反序列化是如何破壞單例的。

  • 序列化和反序列化破壞單例
/**
     * 序列化和反序列化破壞單例
     * */
    public static void test3() throws IOException, ClassNotFoundException {

        //將對象寫到磁盤
        HungarySingleton hungarySingleton = HungarySingleton.getInstance();
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("hungarySingleton.obj"));
        outputStream.writeObject(hungarySingleton);

        System.out.println(hungarySingleton);

        //然后再將對象從磁盤讀取出來
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("hungarySingleton.obj"));
        HungarySingleton object = (HungarySingleton)inputStream.readObject();

        System.out.println(object);
    }
  • 測試結(jié)果
餓漢式=com.example.designpattern.singleton.HungarySingleton@5451c3a8
序列化和反序列化=com.example.designpattern.singleton.HungarySingleton@3d494fbf

上述測試結(jié)果表明 序列化和反序列化也能夠?qū)卫斐善茐模梢蚤喿x源碼找到原因。注意這里我是實現(xiàn)了 Serializable 接口的。

  • 應(yīng)對策略
  1. 不要實現(xiàn) Serializable 接口。序列化和反序列化要求必須實現(xiàn) Serializable 接口,所以為了 防止 序列化和反序列化對單例的破壞,可以不要實現(xiàn) Serializable 接口。
  2. 如果業(yè)務(wù)要求必須要實現(xiàn)Serializable 接口,那么就只有下面這一種方式可以應(yīng)對:重寫 readResolve() 方法。
   /**
     * 防止序列化和反序列化對單例的破壞,返回單例對象
     * @return
     */
    public Object readResolve() {
        return instance;
    }

上面這個返回會在 HungarySingleton object = (HungarySingleton)inputStream.readObject(); 這句話執(zhí)行的時候 回調(diào),直接就返回你自己 返回的對象,所以可以 應(yīng)對 對單例的破壞。具體的可以看看 jdk 的源碼,不能夠找到答案。上面我們實現(xiàn)了餓漢式單例,并且分析了餓漢式單例的優(yōu)缺點,以及反射和序列化,反序列化對單例的破壞。以及相應(yīng)的應(yīng)對策略。下面的內(nèi)容我們就只針對 單例的一些實現(xiàn)展開談?wù)?,對單例的破壞就不做分析了,可以自己去測試。

4.2 懶加載

  • 雙重檢查鎖

在double check 之前我們先來看看 為什么會出現(xiàn) double check 這種寫法。

最簡單的懶加載:
public class SimpleSingleton {

    //1.靜態(tài)成員
    private static SimpleSingleton instance;

    //2. 構(gòu)函數(shù)私有化
    private SimpleSingleton() {}

    //3. 提供全局訪問方法
    public static SimpleSingleton getInstance() {

        if (instance == null) {
            instance = new SimpleSingleton();
        }
        return instance;
    }
}
  • 單線程測試:
 //單線程測試
    public static void test() {
        SimpleSingleton instance = SimpleSingleton.getInstance();
        SimpleSingleton instance2 = SimpleSingleton.getInstance();
        SimpleSingleton instance3 = SimpleSingleton.getInstance();
        System.out.println(instance);
        System.out.println(instance2);
        System.out.println(instance3);
    }

com.example.designpattern.singleton.SimpleSingleton@7440e464
com.example.designpattern.singleton.SimpleSingleton@7440e464
com.example.designpattern.singleton.SimpleSingleton@7440e464

從上面的單線程測試中沒有發(fā)現(xiàn)問題,接下來進行多線程測試,這里我們就使用簡單的多線程測試,也可以并發(fā)測試。

  • 多線程測試
//多線程測試
    public static void test2() {
        for (int i=0 ;i<3; i++) {
            new Thread(()->{
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                SimpleSingleton instance = SimpleSingleton.getInstance();
                System.out.println(instance);
            }).start();
        }
    }

com.example.designpattern.singleton.SimpleSingleton@21279f82
com.example.designpattern.singleton.SimpleSingleton@25f209f5
com.example.designpattern.singleton.SimpleSingleton@6e154e44

這里我們看到直接就產(chǎn)生了3個不同的對象,顯然違背單例的設(shè)計思想。針對線程安全問題,我們可以使用 鎖來解決這個問題。于是就有了下面幾種線程安全的寫法。

  • 靜態(tài)同步方法和同步代碼塊
// 靜態(tài)方法上面 加 synchronized  關(guān)鍵字
public static synchronized SimpleSingleton getInstance() {

        if (instance == null) {
            instance = new SimpleSingleton();
        }
        return instance;
    }

// 同步代碼塊
public static  SimpleSingleton getInstance() {
        synchronized(SimpleSingleton.class) {//這里的鎖 一般這樣寫,但也可以是 靜態(tài)對象 作為鎖
            if (instance == null) {
                instance = new SimpleSingleton();
            }
            return instance;
        }
        
    }

上面的 兩種寫法 效果完全一樣,效率也一樣,鎖的范圍也一樣,都是 類鎖。至于為什么是 類級別的鎖,不是 對象級別的,有下面幾個原因:
1.靜態(tài)方法本身是可以使用類直接調(diào)用,也就在類級別 ,在靜態(tài)方法上面加鎖的化 自然也就是類級別的鎖了;
2 . 靜態(tài)方法里面的 同步代碼塊為啥 是 類級別的鎖。注意這里因為我用的 SimpleSingleton.class 作為鎖,所以說上面的兩種寫法是等效的。一般使用 SimpleSingleton.class作為鎖是 避免創(chuàng)建 其他鎖對象,這里是不能使用 this 作為鎖的,這也是 因為有 static 關(guān)鍵字的原因。
3 . 在這里能不能使用 對象鎖呢?答案是可以的,但是必須是 static 對象 如:

// 創(chuàng)建一個對象作為鎖
    final static Object object = new Object();
    public static  SimpleSingleton getInstance() {
        synchronized(object) {// 使用對象鎖
            if (instance == null) {
                instance = new SimpleSingleton();
            }
            return instance;
        }

    }
//顯然這樣的寫法沒有 SimpleSingleton.class 作為鎖簡單,
//因為你自己又單獨 創(chuàng)建了一個 Object 對象,而且還是 餓漢式創(chuàng)建的。

分析完了上面的 鎖的問題,我們再來分析一下 這種寫法的優(yōu)缺點,是否值得我們平時的項目中使用:
1 .優(yōu)點:synchronized 關(guān)鍵字保證了線程安全,同時也是懶加載的。
2 .缺點:從上面的代碼中我們可以看到 我們的鎖都是全局鎖,也就是說 每一個線程來訪問我們的方法的時候 被要先去 獲得鎖,方法執(zhí)行完了以后再去釋放鎖。我們知道,單例對象只在第一次訪問的時候 創(chuàng)建就ok 了 ,也就是 下面這個邏輯 只在 第一次 訪問該方法的 時候 instance == null ,然后創(chuàng)建 對象。如果在 線程安全的情況下 后續(xù)的線程 的 instance 都是不為空的,就不會去創(chuàng)建對象了,也就保證了線程安全了,那么我們對整個方法 都加上鎖 就很低效了。

   if (instance == null) {// 第一次訪問的時候 滿足
            instance = new SimpleSingleton();
        }

上面我們分析出 靜態(tài)同步方法和同步代碼塊 雖然能夠保證線程安全,但是也帶來可一些性能問題,那么我們就 優(yōu)化一下性能就ok 了。既然instance 只有在 第一次 訪問的 時候 是 null 那么 我們就在 if 里面來 加一個鎖 也就是在 第一次創(chuàng)建的時候 保證 線程安全就ok了,后面的線程 都不會 進入 if ,所以就有了下面的優(yōu)化方案:

public static  SimpleSingleton getInstance() {
            if (instance == null) {
                synchronized (SimpleSingleton.class) {
                    instance = new SimpleSingleton();
                }
            }
            return instance;
    }
  • 測試:
public static void test2() {
        for (int i=0 ;i<3; i++) {
            new Thread(()->{
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                SimpleSingleton instance = SimpleSingleton.getInstance();
                System.out.println(instance);
            }).start();
        }
    }

com.example.designpattern.singleton.SimpleSingleton@6e154e44
com.example.designpattern.singleton.SimpleSingleton@2fd04fd1
com.example.designpattern.singleton.SimpleSingleton@21279f82

測試結(jié)果我們發(fā)現(xiàn),我擦,怎么會呢?不是加了鎖了嗎??我們仔細來分析一下為啥會是這樣的:

public static  SimpleSingleton getInstance() {
            if (instance == null) {// 1. 當(dāng)?shù)谝淮?到這里的就只有一個 線程 1 ,那么這個是線程安全的;2 . 當(dāng)線程 1 和線程 2 同時到 這里了,不管是線程1 和線程 2 誰獲得了鎖 都會是 執(zhí)行 創(chuàng)建對象的 語句,也就有了多個對象。
                synchronized (SimpleSingleton.class) {
                    instance = new SimpleSingleton();
                }
            }
            return instance;
    }

針對上面的問題 我們自然就有了 下面的寫法,也就是我們在 同步代碼塊里面再判斷一次,就可以保證線程安全了,也就有了 雙重檢查鎖的寫法:

public static  SimpleSingleton getInstance() {
            if (instance == null) {// 當(dāng)線程1 和線程2 都執(zhí)行到了這里,假設(shè)線程 1 獲取了鎖
                synchronized (SimpleSingleton.class) {
                    if (instance == null) {
                        instance = new SimpleSingleton();// 線程1 創(chuàng)建了 對象,執(zhí)行完畢 退出 同步代碼塊,釋放鎖,這時候 線程 2 獲取了鎖,然后 讀取了 instance 的值 發(fā)現(xiàn) 不為空 ,第二個 if 條件就不滿足了,不會執(zhí)行 對象創(chuàng)建的語句。
                    }
                }
            }
            return instance;
    }

到這里我們就 把為啥會出現(xiàn) double check 的過程分析了一下,但是 上面 的寫法都還不是 線程安全的,因為 instance = new SimpleSingleton(); 在jvm 創(chuàng)建對象的指令 中 不是原子的,也就說 jvm 創(chuàng)建對象 至少有 下面幾條指令:1. 申請一塊內(nèi)存空間; 2. 創(chuàng)建一個對象;3. 將地址值賦值個 變量;在并發(fā)量高的情況下,可能會發(fā)生可見性問題和指令重排序問題,

https://www.cnblogs.com/goodAndyxublog/p/11356402.html

為了解決這個問題 我們可以 使用 volatile 來 修飾 instance。下面我們來看看 完整的 double check 是怎么寫的。

public class DoubleCheckSingleton {
// volatile  保證可見性,防止指令重排序
   private static volatile DoubleCheckSingleton singleton;
    public static DoubleCheckSingleton doubleCheckSingleton() {
        if (singleton == null) {
            synchronized (DoubleCheckSingleton.class) {
                if (singleton == null ) {//為了防止 兩個線程都進了 第一個if導(dǎo)致的線程安全問題,所以可以再加一次判斷
                    singleton = new DoubleCheckSingleton();
                }
            }
        }
        return singleton;
    }
}

上面我們分析了double check ,一切看上去都很完美,但是就是有一點,使用了線程同步機制,來保證線程的安全性,那么有沒有一種不使用線程同步機制 也可以 實現(xiàn)線程安全和懶加載呢?答案是肯定的,那就是靜態(tài) 內(nèi)部類。另外,在這里我們沒有分析 反射 ,序列化和反序列化對單例的破壞。答案是這兩種都是可以破壞 double check 的單例,可以自己測試一下。接下來我們來分析一下 靜態(tài)內(nèi)部類的單例。

4.2靜態(tài)內(nèi)部類

public class InnerStaticSingleton {

    private InnerStaticSingleton() {}

    //但外部調(diào)用 SingletonHolder.innerStaticSingleton 時才會加載這里的靜態(tài)內(nèi)部類
     private static class SingletonHolder{
        private final static InnerStaticSingleton innerStaticSingleton = new InnerStaticSingleton();

     }

     public static InnerStaticSingleton getInstance() {
        return SingletonHolder.innerStaticSingleton;
     }
}

上面是靜態(tài)內(nèi)部類的實現(xiàn)方式,這里我解釋一下,為什么是懶加載的。在外部類加載到j(luò)vm 時,靜態(tài)內(nèi)部類是不會被加載的,也就不會執(zhí)行 private final static InnerStaticSingleton innerStaticSingleton = new InnerStaticSingleton(); 只有當(dāng) 外部類的 static 方法被調(diào)用時,才會 加載 內(nèi)部類,并實例化對象。靜態(tài)在整個 jvm 運行周期中都只加載一次,所以是可以保證單例的,根據(jù)前面的分析,只有在調(diào)用時才會去初始化對象,所以是懶加載的。至于線程安全,也是利用jvm 內(nèi)部機制保證的,到底是如何保證的,由于筆者水平有限,暫無法解釋,希望大家留言談?wù)摗T摲绞酵ǔ1徽J為是最優(yōu)的 單例實現(xiàn)方式,但是也有一個缺點,就是參數(shù)傳遞的問題。所以到底要使用哪一種實現(xiàn)方式,是取決于 實際應(yīng)用場景的。雖然該方式 優(yōu)雅,但是同樣可以被反射和序列化破壞。那么到底有沒有一種單例是能夠防止反射和序列化的破壞,答案是肯定的,那就是枚舉式單例,終極殺招。

4.3枚舉式單例

public enum EnumSingleton {

    INSTANCE;
}

這就是枚舉式單例,是不是非常簡單。下面我們來測試一下反射和序列化得破壞結(jié)果,看看是否能達到我們的預(yù)期。

  • 反射破壞
public static void test() throws Exception{
        Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        EnumSingleton enumSingleton = declaredConstructor.newInstance();
        System.out.println(enumSingleton);
    }
  • 測試結(jié)果

Exception in thread "main" java.lang.NoSuchMethodException: com.example.designpattern.singleton.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.example.designpattern.singleton.EnumSingleton.test(EnumSingleton.java:23)
at com.example.designpattern.singleton.EnumSingleton.main(EnumSingleton.java:18)

上面是反射的測試結(jié)果,直接給我們異常了,說沒有默認構(gòu)造器,后面我們會 分析一下 底層的原來,看看枚舉單例到底是怎么回事。這里我們再來測試一下序列化。

  • 序列化測試
public static void test2() throws Exception{
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("EnumSingleton.obj"));
        EnumSingleton instance = EnumSingleton.INSTANCE;
        System.out.println(instance);
        outputStream.writeObject(instance);
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("EnumSingleton.obj"));

        EnumSingleton enumSingleton =(EnumSingleton)objectInputStream.readObject();
        System.out.println(enumSingleton);
    }
  • 測試結(jié)果:

INSTANCE
INSTANCE

結(jié)果返回了同一個對象,說明jdk 也為我們屏蔽了序列化對單例的影響。到這來是不是覺得很牛叉,枚舉都幫我們做了,首先是線程安全的,其次反射和序列化也不能破壞它,但是是不是懶加載的呢?肯定不是,因為枚舉也是在jvm 加載的時候就會初始化的。在 Effective java 那本書里面,作者就推薦使用 枚舉式單例。當(dāng)然到底使用哪一種,我們還是要根據(jù)業(yè)務(wù)場景來選擇。好了,既然枚舉這么牛掰,我們能不能看看jvm 在加載 枚舉的時候,到時是怎么做到的?接下來我們就來看看 枚舉的神秘面紗。首先,從代碼層面看不出啥東西,那么,我們就要想辦法看看他編譯后的樣子。

反編輯工具 jad

  • 反編譯 EnumSingleton.class

jad EnumSingleton.class 。會生成一個 EnumSingleton.jad 的文件。

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   EnumSingleton.java

package com.example.designpattern.singleton;

import java.io.*;
import java.lang.reflect.Constructor;

public final class EnumSingleton extends Enum
{
    public static final EnumSingleton INSTANCE;
    private static final EnumSingleton $VALUES[];
// 構(gòu)造器,私有化,沒有無參構(gòu)造器,所以我們在測試的時候 會拋出異常,說沒有無參構(gòu)造器。
 private EnumSingleton(String s, int i)
     {
            super(s, i);
     }
// 靜態(tài)代碼塊初始化對象,餓漢式寫法,是線程安全的。
    static
    {
        INSTANCE = new EnumSingleton("INSTANCE", 0);
        $VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }
    public static EnumSingleton[] values()
    {
        return (EnumSingleton[])$VALUES.clone();
    }
    public static EnumSingleton valueOf(String name)
    {
        return (EnumSingleton)Enum.valueOf(com/example/designpattern/singleton/EnumSingleton, name);
    }
}

上面是 EnumSingleton.class 反編譯的結(jié)果。我們看到 EnumSingleton 枚舉 繼承的 Enum 對象,該對象是 jdk 自帶的java.lang下面的抽象類

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
 protected Enum(String name, int ordinal) {//只有這一個構(gòu)造函數(shù)
        this.name = name;
        this.ordinal = ordinal;
    }
}

上面解釋了我們在反射 調(diào)用無參構(gòu)造函數(shù)的時候,為啥會有異常拋出,那是因為枚舉本身就沒有無參構(gòu)造函數(shù)。好了,到這里可能你又發(fā)現(xiàn)了,雖然沒有無參構(gòu)造函數(shù),但是有 兩個帶參數(shù)的構(gòu)造函數(shù),我們能不能調(diào)用呢?我們來測試一下就知道了。

public static void test() throws Exception{
        Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class); //得到帶有參數(shù)的構(gòu)造函數(shù)
        declaredConstructor.setAccessible(true);
        EnumSingleton enumSingleton = declaredConstructor.newInstance("測試",007);// 調(diào)用,創(chuàng)建對象
        System.out.println(enumSingleton);
    }
  • 測試結(jié)果:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.example.designpattern.singleton.EnumSingleton.test(EnumSingleton.java:25)
at com.example.designpattern.singleton.EnumSingleton.main(EnumSingleton.java:18)

上面結(jié)果說,不能通過反射來創(chuàng)建 枚舉對象。他說在 java.lang.reflect.Constructor.newInstance(Constructor.java:417) 417行 拋出的異常。我們就去看一下 :

  • Constructor
@CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)// 這里的意思是 枚舉的話 就 拋出異常。
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

好了,上面我們就解釋了,為啥枚舉能夠防止反射破壞單例,原來是jdk 幫我們做了這個事情了。我們還有一個序列化破壞沒有找到原因。接下來我們就來看看序列化的原因,由于篇幅太長,可能已經(jīng)忘記了 序列化測試代碼,我們再來貼一下:

public static void test2() throws Exception{
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("EnumSingleton.obj"));
        EnumSingleton instance = EnumSingleton.INSTANCE;
        System.out.println(instance);
        outputStream.writeObject(instance);
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("EnumSingleton.obj"));

        EnumSingleton enumSingleton =(EnumSingleton)objectInputStream.readObject();
        System.out.println(enumSingleton);
    }

上面代碼就兩個意思,1 . 把對象寫到磁盤;2 . 從磁盤讀出來對象。既然讀的時候得到的是一個對象,那么我們就直觀 的先從讀 開始,看看能不能找到答案,如果不能,我們再去 分析寫。

EnumSingleton enumSingleton =(EnumSingleton)objectInputStream.readObject();

看看 readObject()里面

public final Object readObject()
        throws IOException, ClassNotFoundException
    {
        if (enableOverride) {// 這里是false 
            return readObjectOverride();
        }

        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
            Object obj = readObject0(false); //那么 對象就是從這里出來的,我們進去看看
            handles.markDependency(outerHandle, passHandle);
            ClassNotFoundException ex = handles.lookupException(passHandle);
            if (ex != null) {
                throw ex;
            }
            if (depth == 0) {
                vlist.doCallbacks();
            }
            return obj;
        } finally {
            passHandle = outerHandle;
            if (closed && depth == 0) {
                clear();
            }
        }
}

----- enableOverride 我們使用的這個構(gòu)造器
 public ObjectInputStream(InputStream in) throws IOException {
        verifySubclass();
        bin = new BlockDataInputStream(in);
        handles = new HandleTable(10);
        vlist = new ValidationList();
        serialFilter = ObjectInputFilter.Config.getSerialFilter();
        enableOverride = false; // false
        readStreamHeader();
        bin.setBlockDataMode(true);
    }


------ Object obj = readObject0(false);
private Object readObject0(boolean unshared) throws IOException {
        boolean oldMode = bin.getBlockDataMode();
        if (oldMode) {
            int remain = bin.currentBlockRemaining();
            if (remain > 0) {
                throw new OptionalDataException(remain);
            } else if (defaultDataEnd) {
                /*
                 * Fix for 4360508: stream is currently at the end of a field
                 * value block written via default serialization; since there
                 * is no terminating TC_ENDBLOCKDATA tag, simulate
                 * end-of-custom-data behavior explicitly.
                 */
                throw new OptionalDataException(true);
            }
            bin.setBlockDataMode(false);
        }

        byte tc;
        while ((tc = bin.peekByte()) == TC_RESET) {
            bin.readByte();
            handleReset();
        }

        depth++;
        totalObjectRefs++;
        try {
            switch (tc) {
                case TC_NULL:
                    return readNull();

                case TC_REFERENCE:
                    return readHandle(unshared);

                case TC_CLASS:
                    return readClass(unshared);

                case TC_CLASSDESC:
                case TC_PROXYCLASSDESC:
                    return readClassDesc(unshared);

                case TC_STRING:
                case TC_LONGSTRING:
                    return checkResolve(readString(unshared));

                case TC_ARRAY:
                    return checkResolve(readArray(unshared));

                case TC_ENUM: // 前面的 啥邏輯 我們也看不太懂,但是這里可以看到是和枚舉相關(guān)的 ,那么我們 去看看 readEnum(unshared)
                    return checkResolve(readEnum(unshared));

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));

                case TC_EXCEPTION:
                    IOException ex = readFatalException();
                    throw new WriteAbortedException("writing aborted", ex);

                case TC_BLOCKDATA:
                case TC_BLOCKDATALONG:
                    if (oldMode) {
                        bin.setBlockDataMode(true);
                        bin.peek();             // force header read
                        throw new OptionalDataException(
                            bin.currentBlockRemaining());
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected block data");
                    }

                case TC_ENDBLOCKDATA:
                    if (oldMode) {
                        throw new OptionalDataException(true);
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected end of block data");
                    }

                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }
        } finally {
            depth--;
            bin.setBlockDataMode(oldMode);
        }
    }


------ readEnum(unshared)

private Enum<?> readEnum(boolean unshared) throws IOException {
        if (bin.readByte() != TC_ENUM) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);// 這里就是得到對象的 描述 ObjectStreamClass 對象,
        if (!desc.isEnum()) {// 判斷是否是枚舉
            throw new InvalidClassException("non-enum class: " + desc);
        }

        int enumHandle = handles.assign(unshared ? unsharedMarker : null);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(enumHandle, resolveEx);
        }

        String name = readString(false);
        Enum<?> result = null;
        Class<?> cl = desc.forClass();
        if (cl != null) {
            try {
                @SuppressWarnings("unchecked")
                Enum<?> en = Enum.valueOf((Class)cl, name); // 得到 Enum 對象,我們知道 枚舉 是繼承 Enum 的,這里也是 返回的 Enum 。通過 枚舉 class 對象 和 name 就得到 了一個唯一的對象,這個name 就是 我們通常自己定義的 枚舉的對象的name。我們可以繼續(xù) 下去,看看 Enum.valueOf((Class)cl, name); 里面是啥
                result = en;
            } catch (IllegalArgumentException ex) {
                throw (IOException) new InvalidObjectException(
                    "enum constant " + name + " does not exist in " +
                    cl).initCause(ex);
            }
            if (!unshared) {
                handles.setObject(enumHandle, result);
            }
        }

        handles.finish(enumHandle);
        passHandle = enumHandle;
        return result;

----- name 枚舉對象的name
System.out.println(EnumSingleton.INSTANCE.name()); // INSTANCE

----- Enum.valueOf((Class)cl, name); 
Enum :
 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 constant " + enumType.getCanonicalName() + "." + name);
    }


-----------  T result = enumType.enumConstantDirectory().get(name); 
Class<T> enumType:
 Map<String, T> enumConstantDirectory() {
        if (enumConstantDirectory == null) {
            T[] universe = getEnumConstantsShared();
            if (universe == null)
                throw new IllegalArgumentException(
                    getName() + " is not an enum type");
            Map<String, T> m = new HashMap<>(2 * universe.length);
            for (T constant : universe)
                m.put(((Enum<?>)constant).name(), constant); // 保存 枚舉對象
            enumConstantDirectory = m;
        }
        return enumConstantDirectory;
    }
// 該map 不能被序列化
private volatile transient Map<String, T> enumConstantDirectory = null;
// 到這里我們也就看到了 原來 他還是一個 map 集合,map 里面就保存了枚舉對象,在被調(diào)用的時候,就判斷一下,如果是空,就存儲枚舉對象,然后返回,然后通過name 取獲取,每次都是獲取到的一個 對象,這個也叫做 注冊式單例,spring 就是典型的注冊式單例。

上面我們分析了 為啥反序列化得時候 ,得到的也是相同的枚舉對象,就是要因為 jvm 自己講枚舉對象存在了一個map 集合里面,然后每次都是去 map 里面取,對象也就只創(chuàng)建了一次,后面都是 讀出來的自然也就 是單例了,這也就 注冊式單例。到這里我們就分析完了,枚舉能夠防止反射和序列化破壞的原因了。

4.4 ThreadLocal 單例

上面枚舉單例里面提及到了注冊式單例,現(xiàn)在我們來看看另外一種注冊式單例--ThreadLocal 單例。

  • ThreadLocalSingleton
public class ThreadLocalSingleton {

    private ThreadLocalSingleton () {}

    private static final ThreadLocal<ThreadLocalSingleton> threadLocal  = new ThreadLocal<ThreadLocalSingleton>() {
        @Override
        protected ThreadLocalSingleton initialValue() {//初始化值
            return new ThreadLocalSingleton();
        }
    };
    public static ThreadLocalSingleton getInstance() {
        return threadLocal.get();
    }
}

ThreadLocal 本身的特點是變量和線程存在綁定的映射關(guān)系,我們先來看測試結(jié)果,就明白是啥意思了。為了方便理解ThreadLocal 的特點,我們使用線程池來測試。

  • 線程池測試
 public static void test() throws InterruptedException {
// 創(chuàng)建有5個線程的線程池
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 0 ;i<10; i++) {// 開啟10 個線程,有線程池去處理。
            executorService.submit(()->{
                countDownLatch.countDown();
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {

                    e.printStackTrace();
                }

                ThreadLocalSingleton instance = ThreadLocalSingleton.getInstance();
                System.out.println(Thread.currentThread().getName() + "___" + instance);
            });
        }
        countDownLatch.await();
    }
  • 測試結(jié)果
pool-1-thread-5___com.example.designpattern.singleton.ThreadLocalSingleton@6c1553f
pool-1-thread-1___com.example.designpattern.singleton.ThreadLocalSingleton@6f08d27f
pool-1-thread-5___com.example.designpattern.singleton.ThreadLocalSingleton@6c1553f
pool-1-thread-3___com.example.designpattern.singleton.ThreadLocalSingleton@3f46008f
pool-1-thread-2___com.example.designpattern.singleton.ThreadLocalSingleton@d85fdfa
pool-1-thread-4___com.example.designpattern.singleton.ThreadLocalSingleton@447a380d
pool-1-thread-5___com.example.designpattern.singleton.ThreadLocalSingleton@6c1553f
pool-1-thread-3___com.example.designpattern.singleton.ThreadLocalSingleton@3f46008f
pool-1-thread-1___com.example.designpattern.singleton.ThreadLocalSingleton@6f08d27f
pool-1-thread-2___com.example.designpattern.singleton.ThreadLocalSingleton@d85fdfa

/**
 * 注冊式單例
 */
public class RegisterSingleton {

    private static Map<String,RegisterSingleton> map = new ConcurrentHashMap<>(1);

    private RegisterSingleton() {}
    private static volatile RegisterSingleton instance;
    public static RegisterSingleton getInstance() {
        instance = map.get("instance");
       if (null == instance) {
           synchronized (RegisterSingleton.class) {
               instance = map.get("instance");
               if (null == instance) {
                   instance = new RegisterSingleton();
                   map.put("instance",instance);
               }
           }
       }
       return instance;
    }

    public static void main(String[] args) {
        for (int i=0;i<100; i++) {
            new Thread(()->{
                RegisterSingleton instance = RegisterSingleton.getInstance();
                System.out.println(instance);
            }).start();
        }
    }
}

我們觀察相同線程 獲取到的對象是一樣的,這個就是ThreadLocal 本身的特點,也就說 同一個線程獲取到的對象始終是一個,對單個線程來說 這也就是單例了。但是對于不同的線程來說 是獲取到不同的對象。這個和ThreadLocal 本身的數(shù)據(jù)結(jié)構(gòu)有關(guān)系。我們可以去看一看jdk 的源碼,這里我們就不展開了,內(nèi)部是維護了一個 ThreadLocalMap 靜態(tài)內(nèi)部來存儲當(dāng)前線程的值,所以每個線程都有一個ThreadLocalMap 對象與之對應(yīng),獲取到值也只自己線程的。到此,我們可以總結(jié)出一個結(jié)論:注冊式單例就是 對象創(chuàng)建一次,然后存放到 Map 中,后面去Map 里面直接獲取就ok。

上面我們說了注冊式單例和 ThreadLocal 的單例,注冊式單例的實現(xiàn)方式有很多種,但是唯一不變的就是 底層的數(shù)據(jù)結(jié)構(gòu)一定是 Map 的,然后保證 在訪問Map 的時候 是線程安全的就行。最后介紹一種CAS實現(xiàn)的 單例。

  • CAS 單例--原子引用類
public class CASSingleton {

    private CASSingleton() {
    }

    private final static AtomicReference<CASSingleton> atomicReference = new AtomicReference<>();

    public static CASSingleton getInstance() {

        for (; ; ) {//自旋
            CASSingleton casSingleton = atomicReference.get();
            if (casSingleton != null) {
                return casSingleton;
            }
            casSingleton = new CASSingleton();
            // CSA 操作:如果當(dāng)前的值是 null,就更新 為 casSingleton
            boolean compareAndSet = atomicReference.compareAndSet(null, casSingleton);
            if (compareAndSet) {
                return casSingleton;
            }
        }
    }
  • 測試:
 public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                CASSingleton instance = CASSingleton.getInstance();
                System.out.println(Thread.currentThread().getName() + ":" + instance);
            }).start();
        }
        CASSingleton instance = CASSingleton.getInstance();
        CASSingleton instance2 = CASSingleton.getInstance();
        CASSingleton instance3 = CASSingleton.getInstance();
        CASSingleton instance4 = CASSingleton.getInstance();

        System.out.println(instance);
        System.out.println(instance2);
        System.out.println(instance3);
        System.out.println(instance4);
    }

com.example.designpattern.singleton.CASSingleton@52cc8049
com.example.designpattern.singleton.CASSingleton@52cc8049
com.example.designpattern.singleton.CASSingleton@52cc8049
com.example.designpattern.singleton.CASSingleton@52cc8049
Thread-1:com.example.designpattern.singleton.CASSingleton@52cc8049
Thread-0:com.example.designpattern.singleton.CASSingleton@52cc8049
Thread-3:com.example.designpattern.singleton.CASSingleton@52cc8049
Thread-2:com.example.designpattern.singleton.CASSingleton@52cc8049
Thread-4:com.example.designpattern.singleton.CASSingleton@52cc8049

有關(guān)CAS 相關(guān)知識如果不熟悉的話,可以去學(xué)習(xí)一下并發(fā)編程相關(guān)的知識。

總結(jié)一下:
到此就介紹了單例的常見的實現(xiàn)方式:double check ,靜態(tài)內(nèi)部類,枚舉,餓漢式,ThreadLocal,CAS 單例,注冊式單例,當(dāng)然肯定還有其他的變種寫法,但是根本的原則不會改變-- JVM 中整個生命周期中只存在一個對象實例。同時,也分析了單例被破壞的情況,反射和序列化。當(dāng)然項目中不會故意去破壞,但是無意的破壞是可能的,比如反射破壞。好了,這就是筆者對單例模式的理解,如果不足之處,歡迎留言討論!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容