創建型模式之單例模式(相信你看完會對單例模式有新的認識)

單例模式的定義與特點

單例(Singleton)模式的定義:
是指確保一個類在任何情況下都絕對只有一個實例,隱藏其所有的構造方法,並提供一個全局訪問點。屬於創建型模式。

單例模式有 3 個特點:
1.單例類只有一個實例對象;
2.該單例對象必須由單例類自行創建;
3.單例類對外提供一個訪問該單例的全局訪問點;

單例模式的結構

單例模式是設計模式中最簡單的模式之一,但也是面試時常被問到的。通常,普通類的構造函數是公有的,外部類可以通過“new 構造函數()”來生成多個實例。但是,如果將類的構造函數設爲私有的,外部類就無法調用該構造函數,也就無法生成多個實例。這時該類自身必須定義一個靜態私有實例,並向外提供一個靜態的公有函數用於創建或獲取該靜態私有實例。

單例模式的主要角色如下:
單例類:包含一個實例且能自行創建這個實例的類。
訪問類:使用單例的類。

單例模式的實現

說起單例模式的實現,我們首先會想到的就是“餓漢式"和"懶漢式"下面就詳細的講講。

餓漢式單例

特點:在單例類首次加載的時候就創建實例。
優點:沒有任何鎖,執行效率高,性能高。
缺點:在某些情況下,可能會造成內存的浪費,因爲不管你用不用,它都會在類加載時創建一個對象。

常見的餓漢式單例寫法:

/**
 * 常見的餓漢式單例寫法
 */
public class HungrySingleton {
    //類加載時就創建HungrySingleton這個實例對象
    private static final  HungrySingleton hungrySingleton = new HungrySingleton();
    //私有化構造函數
    private HungrySingleton(){}
    //提供全局訪問點
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

靜態代碼塊寫法:

/**
 * 靜態代碼塊寫法
 * 看來更有逼格,其實和上邊的差不多
 */
public class HungryStaticSingleton {
    //類加載時就創建HungrySingleton這個實例對象
    private static final  HungryStaticSingleton hungryStaticSingleton;
    static {
         hungryStaticSingleton = new HungryStaticSingleton();
    }
    //私有化構造函數
    private HungryStaticSingleton(){}
    //提供全局訪問點
    public static HungryStaticSingleton getInstance(){
        return hungryStaticSingleton;
    }
}

因爲餓漢式的缺點,爲了避免內存造成不必要的浪費,所以出現懶漢式單例。

懶漢式單例

特點:在被外部類調用時才創建實例,解決了餓漢式的內存浪費。
優點:節省內存,減少不必要的內存浪費。

1.最簡單的懶漢式寫法:
缺點:線程不安全

/**
 * 最簡單的懶漢式
 */
public class LazySimpleSingleton {

    private static LazySimpleSingleton lazySimpleSingleton;
    //私有化構造函數
    private LazySimpleSingleton(){}
    //提供全局訪問點,用的時候才創建實例
    public static LazySimpleSingleton getInstance(){
        if (lazySimpleSingleton == null){
            lazySimpleSingleton = new LazySimpleSingleton();
        }
        return lazySimpleSingleton;
    }
}

線程破壞懶漢式單例的事故現場:

測試代碼:

public class LazySimpleSingletonTest {
    @Test
    public void test1(){
        new Thread(()->{
            System.out.println(LazySimpleSingleton.getInstance());}
            ).start();
        new Thread(()->{
            System.out.println(LazySimpleSingleton.getInstance());}
            ).start();
    }
}

多次運行發現兩條線程會有可能會創建出不同的實例對象(如下圖),這就違背了單例模式的定義了。
於是產生了懶漢式的第二種寫法(加鎖)
在這裏插入圖片描述
2.懶漢式第二種寫法(加鎖):
解決了線程不安全問題。
缺點:性能低,加鎖後導致並行變串行

public class LazySimpleSingleton {
    private static LazySimpleSingleton lazySimpleSingleton;
    //私有化構造函數
    private LazySimpleSingleton(){}
    //提供全局訪問點
    public static LazySimpleSingleton getInstance(){
    	//加鎖,解決線程不安全問題
        synchronized (LazySimpleSingleton.class) {
            if (lazySimpleSingleton == null) {
                lazySimpleSingleton = new LazySimpleSingleton();
            }
        }
        return lazySimpleSingleton;
    }
}

3.懶漢式第三種寫法(雙重檢查鎖):
解決了線程不安全問題的同時,也解決了性能低的問題。
注意:變量lazySimpleSingletonvolatile 修飾。因爲線程中是存在指令重排序的問題,變量定義的時候會創建一塊內存,而創建實例的時候也會創建一塊內存,變量要指向創建實例的內存。線程運行中會導致這些順序發生變化,也就是指令重排序的問題。所以加volatile關鍵字修飾保證有序性。
缺點:兩個if判斷導致代碼看起來不是那麼的優雅,可讀性不高。

public class LazySimpleSingleton {
    private static volatile LazySimpleSingleton lazySimpleSingleton;
    //私有化構造函數
    private LazySimpleSingleton(){}
    //提供全局訪問點
    public static LazySimpleSingleton getInstance(){
        //檢查是否需要加鎖阻塞
        if (lazySimpleSingleton == null){
            synchronized (LazySimpleSingleton.class) {
                //檢查是否需要創建新的實例
                if (lazySimpleSingleton == null) {
                    lazySimpleSingleton = new LazySimpleSingleton();
                }
            }
        }
        return lazySimpleSingleton;
    }
}

4.懶漢式第四種寫法(靜態內部類):
特點:利用Java語法的特點,寫法優雅,性能高,懶加載避免了內存浪費。看似已經非常完美啦。
解釋:因爲它創建實例是在靜態內部類中,而靜態內部類在被使用到的時候纔會被加載,所以說避免了內存浪費。

/**
 * 懶漢式靜態內部類寫法
 */
public class LazyStaticInnerClassSimpleSingleton {
    //私有化構造函數
    private LazyStaticInnerClassSimpleSingleton(){}
    //全局訪問點
    private static LazyStaticInnerClassSimpleSingleton getInstance(){
        return StaticInner.Instance;
    }
    //靜態內部類,創建實例
    private static class StaticInner{
        private static final LazyStaticInnerClassSimpleSingleton Instance = new LazyStaticInnerClassSimpleSingleton();
    }
}

上邊說到的懶漢式靜態內部類寫法,看似已經很完美了。其實不然,上邊所說到的所有單例寫法其實有兩個共同的缺點:
1.能夠被反序列化破壞
2.能夠被反射破壞

反序列化破壞單例的事故現場

序列化:就是把內存中對象的狀態轉化爲字節碼的形式,把字節碼通過IO輸出流寫到磁盤上,持久化保存下來。
反序列化:就是將持久化的字節碼內容,通過IO輸入流讀取到內存中,轉化成一個實例對象。
這裏咱們就拿常見的餓漢式單例寫法來演示:

測試代碼:

	@Test
    public void test(){
        HungrySingleton instance1 = HungrySingleton.getInstance();
        try {
            //將instance1對象 從內存寫入到硬盤
            FileOutputStream fos = new FileOutputStream("HungrySingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(instance1);
            oos.flush();
            oos.close();

            //從硬盤讀取到內存
            FileInputStream fis = new FileInputStream("HungrySingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            Object instance2 = ois.readObject();
            ois.close();

            //比較instance1和instance2是否是同一個實例
            System.out.println(instance1 );
            System.out.println(instance2);
            System.out.println(instance1 == instance2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

運行結果: 可以發現,反序列化出來的對象實例和全局訪問點獲取出來的對象實例不相同,即違背了單例原則。
在這裏插入圖片描述

解決辦法:其實很簡單,只需要在單例類中加一個方法:
在這裏插入圖片描述
再次看運行結果
在這裏插入圖片描述

反射破壞單例的事故現場

這裏咱們就拿懶漢式靜態內部類寫法來演示:

測試代碼:

public class LazyStaticInnerClassSimpleSingletonTest {
    @Test
    public void test() throws Exception {
        Class <?> clazz = LazyStaticInnerClassSimpleSingleton.class;
        //獲取所有無參構造器(包括私有)
        Constructor<?> constructor = clazz.getDeclaredConstructor(null);
        //強制訪問
        constructor.setAccessible(true);
        //創建實例
        Object o1 = constructor.newInstance();
        Object o2 = constructor.newInstance();
        //比較是否是同一個實例對象
        System.out.println(o1);System.out.println(o2);System.out.println(o1==o2);
    }
}

運行結果: 可以發現,反射直接就繞過了單例類提供的唯一訪問點。這就是反射破壞單例的事故現場。
在這裏插入圖片描述

解決辦法:可以在無參構造方法裏做個判斷,拋異常。
在這裏插入圖片描述
再來看運行結果
是不是反射就創建不了對象了。但是,但是,本來很優雅的代碼,結果你在構造方法中拋個異常,幾個意思?感覺就很奇怪,這就不能忍了吧。且看下回分析。。。下邊分析啊,新的寫法又要來了,乃金剛不壞之身:序列化破壞不了它,反射也破壞不了它。
在這裏插入圖片描述

枚舉式單例

堪稱完美的單例啊,就下邊一個缺點。。。
優點:優雅的代碼,線程安全且避免了序列化和反射的破壞(因爲反射是創建不了枚舉對象的,直接會報錯,待會看源碼)。
缺點:類初始化時,就創建了這個枚舉,在某些情況下可能造成內存浪費。

/**
 * 枚舉式單例
 */
public enum  EnumSingleton {
    INSTANCE; //這個就是實例
    //定義一個屬性,並提供get,set方法
    private String name;
    public String getName() {return name;}
    public void setName(String name) {this.name = name;}
    //全局訪問點
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

測試代碼:

public class EnumSingletonTest {
    @Test
    public void test(){
    	//創建兩個實例對象
        EnumSingleton instance1 = EnumSingleton.getInstance();
        EnumSingleton instance2 = EnumSingleton.getInstance();
       	/**
         * 比較兩個實例對象是否是同一個
         * 如果是同一個對象實例,那麼返回true,並且用instance2可以獲取到instance1設置的名字
        */
        System.out.println(instance1==instance2);
        instance1.setName("枚舉單例");//用instance1 設置名字
        System.out.println(instance2.getName());//用instance2來獲取名字
    }
}

運行結果:
在這裏插入圖片描述
聊一聊爲什麼枚舉單例不會被反射破壞:
看一段源碼:
找到Constructor這個類中的第416行,判斷如果這個類是被ENUM(枚舉)修飾的,那麼就直接拋出Cannot reflectively create enum objects
靠,爲啥人家拋異常就行,咱們在無參構造裏拋個異常就不優雅了啊? 別問爲啥,人家是官方,人家牛掰!
在這裏插入圖片描述

ThreadLocal單例

特點:保證線程內部的全局唯一,且天生線程安全。
注意:是線程內部全局唯一,也就是說,一個線程一個單例實例,線程之間是相互隔離的。

/**
 * ThreadLocal單例寫法
 */
public class ThreadLocalSingleton {
    //私有化無參構造
    private ThreadLocalSingleton(){}
    //交給ThreadLocal創建實例
    private static final ThreadLocal<ThreadLocalSingleton> instance = new ThreadLocal<ThreadLocalSingleton>(){
        @Override
        protected ThreadLocalSingleton initialValue() {
            return new ThreadLocalSingleton();
        }
    };
    //全局訪問點
    private static ThreadLocalSingleton getInstance(){
        return instance.get();
    }
}

測試代碼:

public class ThreadLocalSingletonTest {
    @Test
    public void test(){
        ThreadLocalSingleton mainInstance1 = ThreadLocalSingleton.getInstance();//主線程實例1
        ThreadLocalSingleton mainInstance2 = ThreadLocalSingleton.getInstance();//主線程實例2
        System.out.println("主線程兩個實例:");
        System.out.println(mainInstance1);
        System.out.println(mainInstance2);

        new Thread(()->{
            ThreadLocalSingleton thread1Instance = ThreadLocalSingleton.getInstance();//thread1線程實例
            System.out.println("thread1線程實例:"+thread1Instance);
        }, "thread1").start();

        new Thread(()->{
            ThreadLocalSingleton thread2Instance = ThreadLocalSingleton.getInstance();//thread2線程實例
            System.out.println("thread2線程實例是否相同:"+thread2Instance);
        }, "thread2").start();
    }
}

運行結果:
可以看出來,主線程創建的兩個實例是相同的,所以說:線程內部創建出來的實例是相同的
但是,每個線程創建出來的實例是不同的,所以說:一個線程一個單例實例,線程之間是相互隔離的。
在這裏插入圖片描述

單例模式總結

優點:
1.在內存中只有一個實例,減少內存開銷
2.可以避免對資源的多重佔用
3.設置全局訪問點,嚴格控制訪問

缺點:
1.沒有接口,擴展困難,如果要擴展,只有修改代碼,沒有其他途徑

重點:
1.私有化構造器
2.保證線程安全
3.延遲加載
4.防止被序列化和反序列化破壞
5.防止被反射破壞

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章