DesignPattern系列__10單例模式

單例模式介紹

單例模式,是為了確保在整個軟件體統(tǒng)中,某個類對象只有一個實例,并且該類通常會提供一個對外獲取該實例的public方法(靜態(tài)方法)。
比如日志、數(shù)據(jù)庫連接池等對象,通常需要且只需要一個實例對象,這就會使用單例模式。

單例模式的7種模式

  1. 餓漢式
    • 靜態(tài)常量
    • 靜態(tài)代碼塊
  2. 懶漢式
    • 線程不安全
    • 同步方法
    • 同步代碼塊
  3. 雙重檢查
  4. 靜態(tài)內(nèi)部類
  5. 枚舉
  6. 容器實現(xiàn)單例模式
  7. 線程池實現(xiàn)單例模式

下面依次來說明一下:

餓漢式(靜態(tài)常量)

通常,我們創(chuàng)建一個對象的方式就是new,但是,當(dāng)我們考慮只創(chuàng)建一個實例的時候,就應(yīng)該禁止外部來通過new的方式進(jìn)行創(chuàng)建。同時,由于無法使用new,你應(yīng)該考慮提供一個獲取單例對象的方式給別人。

思路

1.將構(gòu)造器私有化(防止外部new,但是對反射還是有局限)
2.類的內(nèi)部創(chuàng)建對象
3.對外提供一個獲取實例靜態(tài)的public方法

代碼實現(xiàn):

public class Singleton1 {
    public static void main(String[] args) {
        HungrySingleton hungrySingleton = HungrySingleton.getInstance();
        HungrySingleton hungrySingleton1 = HungrySingleton.getInstance();
        System.out.println(hungrySingleton == hungrySingleton1);
    }
}

class HungrySingleton {
    //1.私有化構(gòu)造器
    private HungrySingleton() {
    }

     // 2.類內(nèi)部創(chuàng)建對象,因為步驟3是static的,
    // 所以實例對象是static的
    private final static HungrySingleton instance = new HungrySingleton();

    //3.對外提供一個獲取對象的方法,
    // 因為調(diào)用方式的目的就是為了獲取對象,
    // 所以該方法應(yīng)該是static的。
    public static HungrySingleton getInstance() {
        return instance;
    }
}

運行程序顯示,我們的確只創(chuàng)建了一個對象實例。

小結(jié)

優(yōu)點:代碼實現(xiàn)比較簡單,在類加載的時候就完成了實例化,同時,該方式能夠避免線程安全問題。
缺點:在類裝載的時候就完成實例化,沒有達(dá)到Lazy Loading的效果。如果從始至終從未使用過這個實例,則會造成內(nèi)存的浪費。
這種方式基于classloder機制避免了多線程的同步問題,不過, instance在類裝載時就實例化,在單例模式中大多數(shù)都是調(diào)用getInstance方法, 但是導(dǎo)致類裝載的原因有很多種, 因此不能確定有其他的方式(或者其他的靜態(tài)方法)導(dǎo)致類裝載,這時候初始化instance就沒有達(dá)到lazy loading的效果。
總結(jié):這種單例模式可以使用,但是可能造成內(nèi)存的浪費。

餓漢式(靜態(tài)代碼塊)

該方式和第一種區(qū)別不大,只是將創(chuàng)建實例放在了靜態(tài)代碼塊中。
由于無法使用new,你應(yīng)該考慮提供一個獲取單例對象的方式給別人。

思路

1.將構(gòu)造器私有化(防止外部new,但是對反射還是有局限)
2.類的內(nèi)部創(chuàng)建對象(通過靜態(tài)代碼塊)
3.對外提供一個獲取實例靜態(tài)的public方法

代碼實現(xiàn):

public class Singleton2 {
    public static void main(String[] args) {
        HungrySingleton hungrySingleton = HungrySingleton.getInstance();
        HungrySingleton hungrySingleton1 = HungrySingleton.getInstance();
        System.out.println(hungrySingleton == hungrySingleton1);
    }
}

class HungrySingleton {
    //1.私有化構(gòu)造器
    private HungrySingleton() {
    }

    // 2.類內(nèi)部創(chuàng)建對象,因為步驟3是static的,
    // 所以實例對象是static的
    private final static HungrySingleton instance;

    static {
        instance = new HungrySingleton();
    }

    //3.對外提供一個獲取對象的方法,
    // 因為調(diào)用方式的目的就是為了獲取對象,
    // 所以該方法應(yīng)該是static的。
    public static HungrySingleton getInstance() {
        return instance;
    }
}

小結(jié)

該方式只是將對象的創(chuàng)建放在靜態(tài)代碼塊中,其優(yōu)點和缺點與第一種方式完全一樣。
總結(jié):這種單例模式可以使用,但是可能造成內(nèi)存的浪費。(同第一種)

懶漢式(線程不安全)

該方式的主要思想就是為了改善餓漢式的缺點,通過懶加載(在使用的時候再去加載),達(dá)到節(jié)約內(nèi)存的目的。
由于無法使用new,你應(yīng)該考慮提供一個獲取單例對象的方式給別人。

思路

1.將構(gòu)造器私有化(防止外部new,但是對反射還是有局限)
2.類的內(nèi)部創(chuàng)建對象,懶加載,在使用的時候才去加載
3.對外提供一個獲取實例靜態(tài)的public方法

代碼實現(xiàn):

public class Singleton3 {
    public static void main(String[] args) {
        TestThread testThread = new TestThread();
        Thread thread = new Thread(testThread);
        Thread thread1 = new Thread(testThread);
        thread.start();
        thread1.start();
    }
}

class LazySingleton {
    //1.私有化構(gòu)造器
    private LazySingleton() {}

    //2.類的內(nèi)部聲明對象
    private volatile static LazySingleton instance;

    //3.對外提供獲取對象的方法
    public static LazySingleton getInstance() {
        //判斷類是否被初始化
        if (instance == null) {
            //第一次使用的時候,創(chuàng)建對象
            instance = new LazySingleton();
        }
        return instance;
    }
}

class TestThread implements Runnable {

    @Override
    public void run() {
        System.out.println("線程" + Thread.currentThread().getName() + "開始執(zhí)行");
        try {
            //為了演示多線程情況
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LazySingleton instance = LazySingleton.getInstance();
        System.out.println("線程" + Thread.currentThread().getName() + "初始化對象" + instance.hashCode());
    }
}

執(zhí)行程序后,發(fā)現(xiàn)了問題:

//運行結(jié)果:
線程Thread-0開始執(zhí)行
線程Thread-1開始執(zhí)行
線程Thread-1初始化對象1391273746
線程Thread-0初始化對象547686109

小結(jié)

優(yōu)點:起到了懶加載的作用,但是只能在單線程情況下使用。
缺點:多線程下不安全,如果一個線程進(jìn)入到if語句中阻滯(還未開始創(chuàng)建對象),另一線程進(jìn)入并通過了if判斷,則會創(chuàng)建多個實例,這一點就違背了單例的目的。
結(jié)論:實際情況下,不要使用這種方式。

懶漢式(線程安全,同步方法)

思路

同上一中方式一樣,但是為了解決多線程安全問題,使用同步方法。

代碼演示:

public class Singleton4 {
    public static void main(String[] args) {
        TestThread testThread = new TestThread();
        Thread thread = new Thread(testThread);
        Thread thread1 = new Thread(testThread);
        thread.start();
        thread1.start();
    }
}

class LazySingleton {
    //1.私有化構(gòu)造器
    private LazySingleton() {}

    //2.類的內(nèi)部聲明對象
    private volatile static LazySingleton instance;

    //3.對外提供獲取對象的方法
    public synchronized static LazySingleton getInstance() {
        //判斷類是否被初始化
        if (instance == null) {
            //第一次使用的時候,創(chuàng)建對象
            instance = new LazySingleton();
        }
        return instance;
    }
}

class TestThread implements Runnable {

    @Override
    public void run() {
        System.out.println("線程" + Thread.currentThread().getName() + "開始執(zhí)行");
        try {
            //為了演示多線程情況
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LazySingleton instance = LazySingleton.getInstance();
        System.out.println("線程" + Thread.currentThread().getName() + "初始化對象" + instance.hashCode());
    }
}

運行結(jié)果如下所示:

線程Thread-1開始執(zhí)行
線程Thread-0開始執(zhí)行
線程Thread-0初始化對象681022576
線程Thread-1初始化對象681022576

小結(jié)

優(yōu)點:起到了懶加載的效果,同時,解決了線程安全問題。
缺點:效率低下,每次想要獲取對象的時候,去執(zhí)行g(shù)etInstance()都是通過同步方法。而且,初始化對象后,再次使用的時候,應(yīng)該直接return這個對象。
總結(jié):可以在多線程條件下使用,但是效率低下,不推薦。

懶漢式(線程安全,同步代碼塊)

思路

同樣是為了解決多線程安全問題,不過采用的是同步代碼塊。首先,最先想到的是:

1.將getInstance()方法體全部加上同步鎖。

代碼實現(xiàn):

public class Singleton5 {
    public static void main(String[] args) {
        TestThread testThread = new TestThread();
        Thread thread = new Thread(testThread);
        Thread thread1 = new Thread(testThread);
        thread.start();
        thread1.start();
    }
}

//對getInstance()的方法體整體加同步代碼塊
class LazySingleton {
    //1.私有化構(gòu)造器
    private LazySingleton() {}

    //2.類的內(nèi)部聲明對象
    private volatile static LazySingleton instance;

    //3.對外提供獲取對象的方法
    public static LazySingleton getInstance() {
        //同步代碼塊
        synchronized (LazySingleton.class) {
            //判斷類是否被初始化
            if (instance == null) {
                //第一次使用的時候,創(chuàng)建對象
                instance = new LazySingleton();
            }
        }
        return instance;
    }
}

class TestThread implements Runnable {

    @Override
    public void run() {
        System.out.println("線程" + Thread.currentThread().getName() + "開始執(zhí)行");
        try {
            //為了演示多線程情況
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LazySingleton instance = LazySingleton.getInstance();
//     LazySingleton1 instance = LazySingleton1.getInstance();
        System.out.println("線程" + Thread.currentThread().getName() + "初始化對象" + instance.hashCode());
    }
}

運行的結(jié)果:

線程Thread-0開始執(zhí)行
線程Thread-1開始執(zhí)行
線程Thread-1初始化對象1419349448
線程Thread-0初始化對象1419349448

這種方式的優(yōu)缺點和同步方法一樣,能夠?qū)崿F(xiàn)多線程安全,但是效率低下。那么,能不能提高一下效率呢?我們發(fā)現(xiàn),每次調(diào)用getInstance()的時候,都要進(jìn)入同步代碼塊,但是,一旦對象初始化后,第二次使用的時候,應(yīng)該能夠直接獲取這個對象才對。
按照這個思路,對代碼進(jìn)行更改(為了說明這個,新建一個類LazySingleton1):

2.只在初始化對象部分加上同步鎖

代碼實現(xiàn):

//為了提高效率,通過if判斷,初始化之前進(jìn)入同步鎖
class LazySingleton1 {
    //1.私有化構(gòu)造器
    private LazySingleton1() {}

    //2.類的內(nèi)部聲明對象
    private volatile static LazySingleton1 instance;

    //3.對外提供獲取對象的方法
    public static LazySingleton1 getInstance() {
        //判斷類是否被初始化
        if (instance == null) {
            //第一次使用的時候,創(chuàng)建對象
            synchronized (LazySingleton1.class) {
                instance = new LazySingleton1();
            }
        }
        return instance;
    }

將類TestClass的run()方法進(jìn)行更改,獲取的實例改為LazySingleton1類型。代碼看上去沒有問題,那么運行效果如何呢:

//運行結(jié)果:
線程Thread-1開始執(zhí)行
線程Thread-0開始執(zhí)行
線程Thread-1初始化對象1368942806
線程Thread-0初始化對象1187311731

那么,我們發(fā)現(xiàn),打臉了,多線程情況下,創(chuàng)建了兩個對象,并未達(dá)到單例的目的。

小結(jié)

  • 對整個方法體加同步代碼塊
    可以達(dá)到要求,優(yōu)缺點同同步方法。
  • 只在初始化對象的代碼添加同步鎖
    不能滿足線程安全要求,實際工作中,不能使用這種方式。

懶漢式(線程安全,雙重檢查機制)

思路

針對懶漢式的多線程問題,我們可謂是操碎了心:同步方法可以解決問題,但是效率太低了;同步代碼塊則根本不能保證多線程安全。如何能做到“魚和熊掌兼得”呢?既然同步代碼塊的效率較好,那么我們就針對這個方式進(jìn)行改良:雙重檢查機制,即在getInstance()內(nèi)進(jìn)行兩次檢查,第一次通過if判斷后,初始化對象之前,進(jìn)行同步并再次進(jìn)行判斷。這樣做的目的:既能解決線程安全問題,同時避免第二次使用對象的時候還要執(zhí)行同步的代碼。

代碼實現(xiàn):

public class Singleton6 {
    public static void main(String[] args) {
        TestThread testThread = new TestThread();
        Thread thread = new Thread(testThread);
        Thread thread1 = new Thread(testThread);
        thread.start();
        thread1.start();
    }
}

class LazyDoubleCheckSingleton {
    //1.私有化構(gòu)造器
    private LazyDoubleCheckSingleton() {}

    //2.類的內(nèi)部聲明對象
    private volatile static LazyDoubleCheckSingleton instance;

    //3.對外提供獲取對象的方法
    public static LazyDoubleCheckSingleton getInstance() {
        //判斷類是否被初始化
        if (instance == null) {
            //第一次使用,通過if判斷
            //加鎖
            synchronized (LazyDoubleCheckSingleton.class) {
                //拿到鎖后,初始化對象之前,再次進(jìn)行判斷
                if (instance == null) {
                    instance = new LazyDoubleCheckSingleton();
                }
            }
        }
        return instance;
    }
}

class TestThread implements Runnable {

    @Override
    public void run() {
        System.out.println("線程" + Thread.currentThread().getName() + "開始執(zhí)行");
        try {
            //為了演示多線程情況
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LazyDoubleCheckSingleton instance = LazyDoubleCheckSingleton.getInstance();
        System.out.println("線程" + Thread.currentThread().getName() + "初始化對象" + instance.hashCode());
    }
}

運行結(jié)果如下所示:

//運行結(jié)果:
線程Thread-0開始執(zhí)行
線程Thread-1開始執(zhí)行
線程Thread-1初始化對象996963733
線程Thread-0初始化對象996963733

小結(jié)

優(yōu)點:

  • 解決了上一種方式中的線程安全問題,同時實現(xiàn)了延遲加載的效果,節(jié)約內(nèi)存;
  • 第二次使用的時候,if判斷為false,直接返回創(chuàng)建好的對象,避免進(jìn)入同步代碼,提高了效率;
    結(jié)論:推薦使用這種方式,實際工作中也比較常見這種方式。

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

思路

為了實現(xiàn)多線程情況下安全,除了手工加鎖,還有別的方式?,F(xiàn)在,我們采用靜態(tài)內(nèi)部類的方式。這種方式利用了JVM加載類的機制來保證只初始化一個對象。
思路同樣是私有化構(gòu)造器,對外提供靜態(tài)的公開方法;不同之處是,類的創(chuàng)建交給靜態(tài)內(nèi)部類來時實現(xiàn)。

代碼實現(xiàn)

public class Singleton7 {
    public static void main(String[] args) {
        TestThread testThread = new TestThread();
        Thread thread = new Thread(testThread);
        Thread thread1 = new Thread(testThread);
        thread.start();
        thread1.start();
    }
}

class StaticInnerSingleton {
    // 1.構(gòu)造器私有化
    private StaticInnerSingleton() {}

    // 2.通過靜態(tài)內(nèi)部類來初始化對象
    private static class InnerClass {
        private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
    }

    // 3.對外提供獲取對象的方法
    public static StaticInnerSingleton getInstance() {
        return InnerClass.INSTANCE;
    }
}


class TestThread implements Runnable {

    @Override
    public void run() {
        System.out.println("線程" + Thread.currentThread().getName() + "開始執(zhí)行");
        try {
            //為了演示多線程情況
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        StaticInnerSingleton instance = StaticInnerSingleton.getInstance();
        System.out.println("線程" + Thread.currentThread().getName() + "初始化對象" + instance.hashCode());
    }
}

運行結(jié)果:

線程Thread-0開始執(zhí)行
線程Thread-1開始執(zhí)行
線程Thread-0初始化對象1326533480
線程Thread-1初始化對象1326533480

OK,我們發(fā)現(xiàn),這種方式達(dá)到了預(yù)期的效果。

小結(jié)

優(yōu)點:

  • 這種靜態(tài)內(nèi)部類的方式,通過類加載機制來保證了初始化實例時只有一個實例。
  • 類的靜態(tài)屬性只有在第一次加載類的時候初始化,而JVM能保證線程安全,在類的初始化過程中,只有一個線程能進(jìn)入并完成初始化。
  • 靜態(tài)內(nèi)部類方式實現(xiàn)了懶加載的效果,這種方式不會在類StaticInnerSingleton加載的時候進(jìn)行初始化,而是在第一次使用時調(diào)用getInstance()方法初始化,能夠起到節(jié)約內(nèi)次的目的。
  • 該方式的getInstance()方法,通過調(diào)用靜態(tài)內(nèi)部類的靜態(tài)屬性返回實例對象,避免了每次調(diào)用時進(jìn)行同步,效率高。
    結(jié)論:線程安全,效率高,代碼實現(xiàn)簡單,推薦使用。

枚舉

思路

在靜態(tài)內(nèi)部類的方式中,我們借用了JVM的類加載機制來實現(xiàn)了功能,同樣,還可以借用Java的枚舉來實現(xiàn)單例模式。

public class Singleton8 {
    public static void main(String[] args) {
        TestThread testThread = new TestThread();
        Thread thread = new Thread(testThread);
        Thread thread1 = new Thread(testThread);
        thread.start();
        thread1.start();
    }
}

enum EnumSingleton {
    INSTANCE;

    public void sayHi() {
        System.out.println("Hi, " + INSTANCE);
    }
}

class TestThread implements Runnable {

    @Override
    public void run() {
        System.out.println("線程" + Thread.currentThread().getName() + "開始執(zhí)行");
        try {
            //為了演示多線程情況
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        EnumSingleton instance = EnumSingleton.INSTANCE;
        System.out.println("線程" + Thread.currentThread().getName() + "初始化對象" + instance.hashCode());
    }
}

運行結(jié)果如下:

線程Thread-0開始執(zhí)行
線程Thread-1開始執(zhí)行
線程Thread-1初始化對象1134798663
線程Thread-0初始化對象1134798663

小結(jié)

優(yōu)點:

  • 這中方式需要在JDK1.5以上的版本中使用,利用枚舉來實現(xiàn)單例模式。能避免多線程同步問題。
  • 能防止反序列化重新創(chuàng)建新的對象。
  • 能防止反射機制來破斷單例模式。
    在《Effective Java》中提到了這種方式,其作者推薦。
    結(jié)論:推薦使用。

使用容器來創(chuàng)建單例

思路

我們可以先初始化單例對象,通過容器來管理,然后在使用的時候從容器中獲取對象。

代碼實現(xiàn):

class ContainSingleton {
    private ContainSingleton() {}

    private static Map<String, Object> singletonMap = new HashMap<>();

    public static Object getInstance(String key) {
        return singletonMap.get(key);
    }

    public void putInstance(String key, Object instance) {
        if (StringUtils.isNotEmpty(key) && instance != null) {
            if (!singletonMap.containsKey(key)) {
                singletonMap.put(key,instance);
            }
        }
    }
}

小結(jié)

這種單例模式是有一定的安全隱患的,如果你多個線程去創(chuàng)建實例,并且key相同,是有可能創(chuàng)建多個實例的。這種形式,建議在使用的時候,先去使用一個線程初始化數(shù)據(jù)后再使用。

線程池實現(xiàn)單例模式

思路

思路也前面的幾種形式一樣,無非就是用線程池來創(chuàng)建對象而已。

代碼實現(xiàn)

class ThreadLocalSingleton {
    //私有化構(gòu)造器
    private ThreadLocalSingleton() {}

    //類的內(nèi)部創(chuàng)建單例對象
    private static final ThreadLocal<ThreadLocalSingleton> instanceThreadLocal =
            new ThreadLocal<ThreadLocalSingleton>() {
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };
    // 獲取對象的方法
    public static ThreadLocalSingleton getInstance() {
        return instanceThreadLocal.get();
    }
}

但是,這種形式的單例模式是要帶引號的。為什么這么說呢?寫一個代碼測試一下吧:

class TestClass implements Runnable {
    @Override
    public void run() {
        System.out.println("線程" + Thread.currentThread().getName() + "開始執(zhí)行");
        try {
            //為了演示多線程情況
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        ThreadLocalSingleton instance = ThreadLocalSingleton.getInstance();
        System.out.println("線程" + Thread.currentThread().getName() + "初始化對象" + instance);
    }
}

public class Singleton10 {
    public static void main(String[] args) {
        TestClass testClass = new TestClass();
        Thread t1 = new Thread(testClass);
        Thread t2 = new Thread(testClass);
        t1.start();
        t2.start();

        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
    }
}

OK , 我們發(fā)現(xiàn)了,多線程下創(chuàng)建了不同的對象,但是,對于同一線程,你多次獲取的對象始終是同一個。


image

小結(jié)

這種形式的單例模式,和之前的懶漢式加鎖的形式不一樣,加同步鎖的思路是犧牲時間(效率)來實現(xiàn);這種形式是保證同一線程中的單例,
屬于犧牲空間來實現(xiàn)。

單例模式的序列化漏洞

在上面的枚舉類的總結(jié)中,我們提高枚舉方式能夠避免反序列化對象的時候重新建立新的對象(反序列化漏洞),那么什么是反序列化漏洞呢?Java對象進(jìn)行反序列化的時候會通過反射機制來創(chuàng)建實例,反射機制的存在使得我們可以越過Java本身的靜態(tài)檢查和類型約束,在運行期直接訪問和修改目標(biāo)對象的屬性和狀態(tài)。這里理解的不是很準(zhǔn)確,有錯誤的話請指出。

代碼演示:

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
//        HungrySingleton instance = HungrySingleton.getInstance();
//        //序列化
//        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serializable_singleton"));
//        oos.writeObject(instance);
//
//        //反序列化
//        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("serializable_singleton"));
//        HungrySingleton newInstance = (HungrySingleton) ois.readObject();

        LazyDoubleCheckSingleton instance = LazyDoubleCheckSingleton.getInstance();
        //序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serializable_singleton"));
        oos.writeObject(instance);

        //反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("serializable_singleton"));
        LazyDoubleCheckSingleton newInstance = (LazyDoubleCheckSingleton) ois.readObject();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }

}
class HungrySingleton implements Serializable {


    private static final long serialVersionUID = -4913346286867374832L;

    //1.私有化構(gòu)造器
    private HungrySingleton() {
    }

    // 2.類內(nèi)部創(chuàng)建對象,因為步驟3是static的,
    // 所以實例對象是static的
    private final static HungrySingleton instance;

    static {
        instance = new HungrySingleton();
    }

    //3.對外提供一個獲取對象的方法,
    // 因為調(diào)用方式的目的就是為了獲取對象,
    // 所以該方法應(yīng)該是static的。
    public static HungrySingleton getInstance() {
        return instance;
    }

    //解決單例模式的反序列化漏洞
  //    public Object readResolve() {
//        return instance;
//    }
}

class LazyDoubleCheckSingleton implements Serializable {
    private static final long serialVersionUID = -8459475238793042042L;

    //1.私有化構(gòu)造器
    private LazyDoubleCheckSingleton() {}

    //2.類的內(nèi)部聲明對象
    private volatile static LazyDoubleCheckSingleton instance;

    //3.對外提供獲取對象的方法
    public static LazyDoubleCheckSingleton getInstance() {
        //判斷類是否被初始化
        if (instance == null) {
            //第一次使用,通過if判斷
            //加鎖
            synchronized (LazyDoubleCheckSingleton.class) {
                //拿到鎖后,初始化對象之前,再次進(jìn)行判斷
                if (instance == null) {
                    instance = new LazyDoubleCheckSingleton();
                }
            }
        }
        return instance;
    }

//    public Object readResolve() {
//        return instance;
//    }
}

這里,我們分別提供了懶漢式和餓漢式(雙重檢查)來驗證這個現(xiàn)象。運行后會報錯,實現(xiàn)Serializable接口后能夠正常運行,結(jié)果如下:

com.bm.desginpattern.pattern.creational.singleton.serialization.LazyDoubleCheckSingleton@7f31245a
com.bm.desginpattern.pattern.creational.singleton.serialization.LazyDoubleCheckSingleton@6d03e736
false

創(chuàng)建了兩個對象,沒有實現(xiàn)多線程安全。首先說明一下解決方案,然后再講解一下原理。我們發(fā)現(xiàn)餓漢式還是懶漢式都新增了一個方法readResolve(),將注釋取消后,再次運行的結(jié)果如下:

com.bm.desginpattern.pattern.creational.singleton.serialization.LazyDoubleCheckSingleton@7f31245a
com.bm.desginpattern.pattern.creational.singleton.serialization.LazyDoubleCheckSingleton@7f31245a
true

奇跡出現(xiàn)了,只是增加一個方法,情況完全不同了。那么背后的原理是什么呢?我們通過debug來講解:


image

1.在23行打一個斷點,進(jìn)入并進(jìn)入該方法:


image

2.我們發(fā)現(xiàn),該方法首先是進(jìn)行一些判斷,然后執(zhí)行readObject0()方法,進(jìn)入該方法查看:
image
//該方法完成代碼
 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:
                    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);
        }
    }

我們發(fā)現(xiàn),該方法還是對傳入的對象進(jìn)行一些判斷,在這里,我們匹配到TC_OBJECT,執(zhí)行對應(yīng)的方法。
3.進(jìn)入該方法:


image

4.進(jìn)一步查看:


image

我們看到一個名為resolveEx的屬性,說明很接近了。
5.繼續(xù)往下調(diào)試:
image

image

我們發(fā)現(xiàn),這三個條件都滿足,因為我們在LazyDoubleCheckSingleton類中定義了readResolve()方法。


image

6.if判斷通過,進(jìn)入到下一個方法:
image

7.在該方法中,我們發(fā)現(xiàn)經(jīng)過一些條件判斷后,通過反射方式來調(diào)用我們在類LazyDoubleCheckSingleton中新定義的方法readResolve():
image
  • 如果我們沒有新增這個方法,反射的時候會新建一個LazyDoubleCheckSingleton對象,并將其返回;
  • 當(dāng)我們新增這個readResolve()的時候,反射的時候還是會創(chuàng)建一個新的對象,但是,返回的是我們在readResolve()中的定義的返回對象。從而達(dá)到了多線程安全的目的。

單例模式的反射漏洞

除了反序列化漏洞,單例模式還有反射漏洞。下面介紹一下:
通過反射,能夠破壞單例模式,進(jìn)而生成多個對象。

先來一個例子,以餓漢式為例:

class HungrySingleton {
    private HungrySingleton() {}

    private final static HungrySingleton instance = new HungrySingleton();

    public static HungrySingleton getInstance() {
        return instance;
    }
}

  public static void main(String[] args) throws Exception {
        //測試,餓漢式
        Constructor<HungrySingleton> constructor = HungrySingleton.class
                .getDeclaredConstructor();
        constructor.setAccessible(true);
        HungrySingleton instance = HungrySingleton.getInstance();
        HungrySingleton newInstance = constructor.newInstance();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }

運行一下,就能發(fā)現(xiàn),生成了兩個實例,破壞了單例模式。同樣的情況,也會發(fā)生在靜態(tài)內(nèi)部類、懶漢式中。

解決方案

  • 餓漢式、靜態(tài)內(nèi)部類:
    直接改造一下構(gòu)造器即可,防止生成多個對象。
 private HungrySingleton() {
        if (instance != null) {
            throw new RuntimeException("禁止反射機制生成實例");
        }
    }

靜態(tài)內(nèi)部類同理。

  • 懶漢式:
    當(dāng)你采用懶漢式的時候,關(guān)于防止反射攻擊,我是比較悲觀的。當(dāng)然,解決問題的思路和餓漢式一樣,但是效果卻不盡人意。代碼演示如下:
    首先,改造構(gòu)造器。
 private HungrySingleton() {
        if (instance != null) {
            throw new RuntimeException("單例構(gòu)造器禁止反射機制調(diào)用");
        }
    }

但是,當(dāng)你先執(zhí)行g(shù)etInstance()方法來生成實例的時候,問題能夠解決,可以當(dāng)你先通過反射來生成對象的時候,就出問題了:


image

這時,你的運行結(jié)果就如下圖所示:


image

怎么辦?有人說,新增一個變量,在構(gòu)造器中根據(jù)變量的值該判斷,但是,這種方式其實沒啥用。因為同樣可以通過反射機制該修改屬性值。
在這里,再一次想起神奇的枚舉類,既能防止反序列化漏洞,又能防止反射漏洞,推薦大家使用。

單例模式在框架源碼中的使用

jdk中的使用案例

例如Runtime類,使用的就是單例模式的餓漢式(Runtime類在lang包中,在JVM運行的時候就被加載)來實現(xiàn):


image

還有Desktop類,使用的就是單例模式的容器模式結(jié)合同步鎖來實現(xiàn)的:


image

Spring中單例模式的應(yīng)用

Spring單例Bean與單例模式的區(qū)別:它們關(guān)聯(lián)的環(huán)境不一樣,單例模式是指在一個JVM進(jìn)程中僅有一個實例,而Spring單例是指一個Spring Bean容器(ApplicationContext)中僅有一個實例。


image

image

當(dāng)你配置一個bean為單例的時候(默認(rèn)就是singleton),在獲取對象的時候,spring會讀取判斷為true,然后如果這個對象已經(jīng)創(chuàng)建好則直接返回,否則就調(diào)用方法getEarlySingletonInstance()來創(chuàng)建對象(其源碼為第二張圖片)。

總結(jié)

  • 單例模式保證了 系統(tǒng)內(nèi)存中該類只存在一個對象,節(jié)省了系統(tǒng)資源,對于一些需要頻繁創(chuàng)建銷毀的對象,使用單例模式可以提高系統(tǒng)性能。
  • 當(dāng)想實例化一個單例類的時候,必須要記住使用相應(yīng)的獲取對象的方法,而不是使用new。
  • 單例模式使用的場景:需要頻繁的進(jìn)行創(chuàng)建和銷毀的對象、創(chuàng)建對象時耗時過多或耗費資源過多(即:重量級對象), 但又經(jīng)常用到的對象、工具類對象、頻繁訪問數(shù)據(jù)庫或文件的對象(比如數(shù)據(jù)源、 session工廠等)。
最后編輯于
?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

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