java實(shí)現(xiàn)設(shè)計(jì)模式--單例模式

單例模式(Singleton Pattern)是 Java 中最簡單的設(shè)計(jì)模式之一。這種類型的設(shè)計(jì)模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對(duì)象的最佳方式。
這種模式涉及到一個(gè)單一的類,該類負(fù)責(zé)創(chuàng)建自己的對(duì)象,同時(shí)確保只有單個(gè)對(duì)象被創(chuàng)建。這個(gè)類提供了一種訪問其唯一的對(duì)象的方式,可以直接訪問,不需要實(shí)例化該類的對(duì)象。

注意:

1、單例類只能有一個(gè)實(shí)例。

2、單例類必須自己創(chuàng)建自己的唯一實(shí)例。

3、單例類必須給所有其他對(duì)象提供這一實(shí)例。

單例模式常用的兩種:餓漢式單例模式;懶漢式單例模式。

1.餓漢式單例模式:

顧名思義餓漢式單例模式是說在類加載的時(shí)候就已經(jīng)將類給初始化在內(nèi)存中,從而后續(xù)的調(diào)用的都是從內(nèi)存中獲取這一個(gè)類的初始化實(shí)例對(duì)象,所有引用的都指向同一個(gè)地址。

餓漢式單例模式:

/**
 * @author: 
 * @date: 2022/2/21 15:45
 * @description:  餓漢式單例模式:類加載的時(shí)候就會(huì)自動(dòng)構(gòu)造產(chǎn)生一個(gè)單例對(duì)象
 */
public class HungrySingleton {
    /**
     * 定義類屬性
     */
    private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();

    /**
     * 私有化構(gòu)造方法
     */
    private HungrySingleton() {
    }

    /**
     * 對(duì)外暴露拿到實(shí)例的方法
     * @return
     */
     public static HungrySingleton getInstance(){
        return HUNGRY_SINGLETON;
     }
}

單線程測試類:

public class Test {
    public static void main(String[] args) throws Exception {
       HungrySingleton singleton1 =  HungrySingleton.getInstance();
       HungrySingleton singleton2 =  HungrySingleton.getInstance();
       System.out.println(singleton1);
       System.out.println(singleton2);
    }
}

打印結(jié)果:

design.singleton.HungrySingleton@2b193f2d
design.singleton.HungrySingleton@2b193f2d

Process finished with exit code 0

多線程測試:

 */
public class Test {
    public static void main(String[] args) throws Exception {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
                Test.println();
            }).start();
            countDownLatch.await();
        }
        countDownLatch.countDown();
    }
    private static void println(){
        System.out.println(HungrySingleton.getInstance());
    }
}

多線程測試結(jié)果:


餓漢式單例模式多線程測試結(jié)果.png

兩種測試結(jié)果(多線程的可以多測試幾次),返回的結(jié)果都是一樣的,說明餓漢式單例模式是線程安全的。

2.懶漢式單例模式:

在類加載時(shí),就給該類創(chuàng)建一個(gè)可用的內(nèi)存空間,但是具體的實(shí)例對(duì)象需要在使用時(shí)自己用new的方式初始化一次,之后所有其他引用的實(shí)例地址均指向第一次new的地址。

/**
 * @author: 
 * @date: 2022/2/21 18:09
 * @description: 懶漢式單例模式
 */
public class LazyUnSafeSingleton {
    /**
     * 類成員屬性
     */
    private static LazyUnSafeSingleton instance;

    /**
     * 私有化構(gòu)造方法
     */
    private LazyUnSafeSingleton() {
    }

    /**
     * 對(duì)外暴露拿到實(shí)例的方法
     * @return
     */
    public static LazyUnSafeSingleton getInstance() {
        if (instance == null) {
            instance = new LazyUnSafeSingleton();
        }
        return instance;
    }
}

一般測試:

public class Test {
    public static void main(String[] args) throws Exception {
        LazyUnSafeSingleton singleton1 = LazyUnSafeSingleton.getInstance();
        LazyUnSafeSingleton singleton2 = LazyUnSafeSingleton.getInstance();
        System.out.println(singleton1);
        System.out.println(singleton2);
    }
}

一般測試結(jié)果:

design.singleton.LazyUnSafeSingleton@2b193f2d
design.singleton.LazyUnSafeSingleton@2b193f2d

Process finished with exit code 0

可以看出singleton1與singleton2的地址是一樣的,兩次實(shí)例的對(duì)象引用地址一致。但是第一次引用時(shí),才開始new一個(gè)實(shí)例,之前在類加載過程中并未直接實(shí)例化。

多線程測試:

public class Test {
    public static void main(String[] args) throws Exception {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
                Test.println();
            }).start();
            countDownLatch.await();
        }
        countDownLatch.countDown();
    }

    private static void println(){
        System.out.println(LazyUnSafeSingleton.getInstance());
    }
}

多線程測試結(jié)果:


image.png

一般測試沒問題,多線程測試出現(xiàn)了對(duì)象不一樣的情況!,說明多線程下的一般懶漢單例模式屬于線程不安全的。

一般的懶漢式單例模式優(yōu)化后

演化成雙重檢查鎖單例模式(DCL)

/**
 * @author: 
 * @date: 2022/2/21 19:25
 * @description:
 */
public class LazySafeSingleton {
    /**
     * 類成員屬性
     */
    private static volatile LazySafeSingleton instance;

    /**
     * 私有化構(gòu)造方法
     */
    private LazySafeSingleton() {
    }

    /**
     * 對(duì)外暴露拿到實(shí)例的方法
     * @return
     */
    public static LazySafeSingleton getInstance() {
        if (instance == null) {
            synchronized (LazySafeSingleton.class) {
                if (instance == null) {
                    instance = new LazySafeSingleton();
                }
            }
        }
        return instance;
    }
}

多線程測試:

public class Test {
    public static void main(String[] args) throws Exception {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
                Test.println();
            }).start();
            countDownLatch.await();
        }
        countDownLatch.countDown();
    }

    private static void println(){
        System.out.println(LazySafeSingleton.getInstance());
    }
}

多線程測試結(jié)果:


DCL單例模式多線程測試結(jié)果.png

可以看到結(jié)果如我們的預(yù)期,大家可以多次執(zhí)行看看會(huì)不會(huì)出現(xiàn)不一樣的對(duì)象。

這兩種單例真的就不能被創(chuàng)建出多例嗎?

我們嘗試用反射去創(chuàng)建對(duì)象,看看效果

代碼:

public class Test {
    public static void main(String[] args) throws Exception {
        // 通過單例模式拿到對(duì)象
        HungrySingleton singleton1 =  HungrySingleton.getInstance();
        // 通過反射創(chuàng)建對(duì)象
        Class singletonClass = HungrySingleton.class;
        Constructor[] cts = singletonClass.getDeclaredConstructors();
        // 訪問權(quán)限打開setAccessible(true),就可以訪問私有構(gòu)造函數(shù)
        cts[0].setAccessible(true);  
        HungrySingleton singleton2 = (HungrySingleton)cts[0].newInstance();
        System.out.println(singleton1);
        System.out.println(singleton2);
        }
}

執(zhí)行結(jié)果:

design.singleton.HungrySingleton@2b193f2d
design.singleton.HungrySingleton@355da254

Process finished with exit code 0

兩種方式拿到的對(duì)象地址不一樣,說明不是一個(gè)對(duì)象。

結(jié)論:

一般的餓漢式和懶漢式單例模式都是可以被反射給破壞的。

如何解決?

可以使用 枚舉類單例模式 解決這個(gè)問題
枚舉類單例模式:

/**
 * @author: 
 * @date: 2022/2/21 15:45
 * @description:  枚舉式單例模式
 */
public enum EnumSingleton {

    /**
     * 枚舉自身
     */
    INSTANCE;

    /**
     * 做業(yè)務(wù)邏輯
     * @return
     */
    public void doSomeThing(){
        System.out.println("實(shí)現(xiàn)你這個(gè)類需要做的一些事");
    }
}

測試類:

/**
 * @author: dingshitai
 * @date: 2022/2/21 15:50
 * @description:
 */
public class Test {
    public static void main(String[] args) throws Exception {
        // 只能通過這種方式去調(diào)用類里面的方法
        EnumSingleton.INSTANCE.doSomeThing();
    }

可以看出我們使用了枚舉類單例模式,只能用這種方式去調(diào)用類內(nèi)部的方法。

那可以被反射破壞嗎?

不能。
上代碼:

public class Test {
    public static void main(String[] args) throws Exception {
        // 通過反射創(chuàng)建對(duì)象
        Class EnumSingletonClass = EnumSingleton.class;
        Constructor[] cts = EnumSingletonClass.getDeclaredConstructors();
        // 訪問權(quán)限打開setAccessible(true),就可以訪問私有構(gòu)造函數(shù)
        cts[0].setAccessible(true);
        EnumSingleton singleton1 = (EnumSingleton) cts[0].newInstance();
        System.out.println(singleton1);
    }
}

測試結(jié)果:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at design.singleton.Test.main(Test.java:18)

Process finished with exit code 1

枚舉類是不能被反射創(chuàng)建出實(shí)例對(duì)象的??!
為啥呢?
原因就在這行

        EnumSingleton singleton1 = (EnumSingleton) cts[0].newInstance();

進(jìn)入 newInstance() 方法中
可以看到反射的源碼:

反射實(shí)例化方法.png

類對(duì)象類型是ENUM類型的時(shí)候,會(huì)直接報(bào)出錯(cuò)誤,從而阻止反射生成實(shí)例化對(duì)象,這就是最根本的原因。
所以如果想要用到單例模式,既考慮到安全性又考慮到并發(fā)性,建議使用枚舉類單例模式。

最后聊一聊spring源碼中用到單例模式的地方
在DefaultSingletonBeanRegistry這個(gè)注冊類中去根據(jù)beanName生成對(duì)象的時(shí)候

image.png

spring用到了單例模式。

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

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

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