1. 深入理解單例設(shè)計模式

        共 16361字,需瀏覽 33分鐘

         ·

        2021-06-13 20:28

        作者:惜鳥
        來源:Segmentfault 思否

        一、概述

        單例模式是面試中經(jīng)常會被問到的一個問題,網(wǎng)上有大量的文章介紹單例模式的實現(xiàn),本文也是參考那些優(yōu)秀的文章來做一個總結(jié),通過自己在學(xué)習(xí)過程中的理解進(jìn)行記錄,并補(bǔ)充完善一些內(nèi)容,一方面鞏固自己所學(xué)的內(nèi)容,另一方面希望能對其他同學(xué)提供一些幫助。
        本文主要從以下幾個方面介紹單例模式:
        1. 單例模式是什么
        2. 單例模式的使用場景
        3. 單例模式的優(yōu)缺點
        4. 單例模式的實現(xiàn)(重點)
        5. 總結(jié)

        二、單例模式是什么

        23 種設(shè)計模式可以分為三大類:創(chuàng)建型模式、行為型模式、結(jié)構(gòu)型模式。單例模式屬于創(chuàng)建型模式的一種,單例模式是最簡單的設(shè)計模式之一:單例模式只涉及一個類,確保在系統(tǒng)中一個類只有一個實例,并提供一個全局訪問入口。許多時候整個系統(tǒng)只需要擁有一個全局對象,這樣有利于我們協(xié)調(diào)系統(tǒng)整體的行為。

        三、單例模式的使用場景

        1、 日志類
        日志類通常作為單例實現(xiàn),并在所有應(yīng)用程序組件中提供全局日志訪問點,而無需在每次執(zhí)行日志操作時創(chuàng)建對象。
        2、 配置類
        將配置類設(shè)計為單例實現(xiàn),比如在某個服務(wù)器程序中,該服務(wù)器的配置信息存放在一個文件中,這些配置數(shù)據(jù)由一個單例對象統(tǒng)一讀取,然后服務(wù)進(jìn)程中的其他對象再通過這個單例對象獲取這些配置信息,這種方式簡化了在復(fù)雜環(huán)境下的配置管理。
        3、工廠類
        假設(shè)我們設(shè)計了一個帶有工廠的應(yīng)用程序,以在多線程環(huán)境中生成帶有 ID 的新對象(Acount、Customer、Site、Address 對象)。如果工廠在 2 個不同的線程中被實例化兩次,那么 2 個不同的對象可能有 2 個重疊的 id。如果我們將工廠實現(xiàn)為單例,我們就可以避免這個問題,結(jié)合抽象工廠或工廠方法和單例設(shè)計模式是一種常見的做法。
        4、以共享模式訪問資源的類
        比如網(wǎng)站的計數(shù)器,一般也是采用單例模式實現(xiàn),如果你存在多個計數(shù)器,每一個用戶的訪問都刷新計數(shù)器的值,這樣的話你的實計數(shù)的值是難以同步的。但是如果采用單例模式實現(xiàn)就不會存在這樣的問題,而且還可以避免線程安全問題。
        5、在Spring中創(chuàng)建的Bean實例默認(rèn)都是單例模式存在的。
        適用場景:
        • 需要生成唯一序列的環(huán)境
        • 需要頻繁實例化然后銷毀的對象。
        • 創(chuàng)建對象時耗時過多或者耗資源過多,但又經(jīng)常用到的對象。
        • 方便資源相互通信的環(huán)境

        四、單例模式的優(yōu)缺點

        優(yōu)點:
        • 在內(nèi)存中只有一個對象,節(jié)省內(nèi)存空間;
        • 避免頻繁的創(chuàng)建銷毀對象,減輕 GC 工作,同時可以提高性能;
        • 避免對共享資源的多重占用,簡化訪問;
        • 為整個系統(tǒng)提供一個全局訪問點。
        缺點:
        • 不適用于變化頻繁的對象;
        • 濫用單例將帶來一些負(fù)面問題,如為了節(jié)省資源將數(shù)據(jù)庫連接池對象設(shè)計為的單例類,可能會導(dǎo)致共享連接池對象的程序過多而出現(xiàn)連接池溢出;
        • 如果實例化的對象長時間不被利用,系統(tǒng)會認(rèn)為該對象是垃圾而被回收,這可能會導(dǎo)致對象狀態(tài)的丟失;

        五、單例模式的實現(xiàn)(重點)

        實現(xiàn)單例模式的步驟如下:
        1. 私有化構(gòu)造方法,避免外部類通過 new 創(chuàng)建對象
        2. 定義一個私有的靜態(tài)變量持有自己的類型
        3. 對外提供一個靜態(tài)的公共方法來獲取實例
        4. 如果實現(xiàn)了序列化接口需要保證反序列化不會重新創(chuàng)建對象

        1、餓漢式,線程安全

        餓漢式單例模式,顧名思義,類一加載就創(chuàng)建對象,這種方式比較常用,但容易產(chǎn)生垃圾對象,浪費內(nèi)存空間。
        優(yōu)點:線程安全,沒有加鎖,執(zhí)行效率較高
        缺點:不是懶加載,類加載時就初始化,浪費內(nèi)存空間
        懶加載 (lazy loading):使用的時候再創(chuàng)建對象
        餓漢式單例是如何保證線程安全的呢?它是基于類加載機(jī)制避免了多線程的同步問題,但是如果類被不同的類加載器加載就會創(chuàng)建不同的實例。
        代碼實現(xiàn),以及使用反射破壞單例:
        /**
         * 餓漢式單例測試
         *
         * @className: Singleton
         * @date: 2021/6/7 14:32
         */
        public class Singleton  {
            // 1、私有化構(gòu)造方法
            private Singleton(){}
            // 2、定義一個靜態(tài)變量指向自己類型
            private final static Singleton instance = new Singleton();
            // 3、對外提供一個公共的方法獲取實例
            public static Singleton getInstance() {
                return instance;
            }

        }
        使用反射破壞單例,代碼如下:
        public class Test {

            public static void main(String[] args) throws Exception{
                // 使用反射破壞單例
                // 獲取空參構(gòu)造方法
                Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);
                // 設(shè)置強(qiáng)制訪問
                declaredConstructor.setAccessible(true);
                // 創(chuàng)建實例
                Singleton singleton = declaredConstructor.newInstance();
                System.out.println("反射創(chuàng)建的實例" + singleton);
                System.out.println("正常創(chuàng)建的實例" + Singleton.getInstance());
                System.out.println("正常創(chuàng)建的實例" + Singleton.getInstance());
            }
        }
        輸出結(jié)果如下:
        反射創(chuàng)建的實例com.example.spring.demo.single.Singleton@6267c3bb
        正常創(chuàng)建的實例com.example.spring.demo.single.Singleton@533ddba
        正常創(chuàng)建的實例com.example.spring.demo.single.Singleton@533ddba

        2、懶漢式,線程不安全

        這種方式在單線程下使用沒有問題,對于多線程是無法保證單例的,這里列出來是為了和后面使用鎖保證線程安全的單例做對比。
        優(yōu)點:懶加載

        缺點:線程不安全
        代碼實現(xiàn)如下:
        /**
         * 懶漢式單例,線程不安全
         *
         * @className: Singleton
         * @date: 2021/6/7 14:32
         */
        public class Singleton  {
            // 1、私有化構(gòu)造方法
            private Singleton(){ }
            // 2、定義一個靜態(tài)變量指向自己類型
            private static Singleton instance;
            // 3、對外提供一個公共的方法獲取實例
            public static Singleton getInstance() {
                // 判斷為 null 的時候再創(chuàng)建對象
                if (instance == null) {
                    instance = new Singleton();
                }
                return instance;
            }
        }
        使用多線程破壞單例,測試代碼如下:
        public class Test {

            public static void main(String[] args) {
                for (int i = 0; i < 3; i++) {
                    new Thread(() -> {
                        System.out.println("多線程創(chuàng)建的單例:" + Singleton.getInstance());
                    }).start();
                }
            }
        }
        輸出結(jié)果如下:
        多線程創(chuàng)建的單例:com.example.spring.demo.single.Singleton@18396bd5
        多線程創(chuàng)建的單例:com.example.spring.demo.single.Singleton@7f23db98
        多線程創(chuàng)建的單例:com.example.spring.demo.single.Singleton@5000d44

        3、懶漢式,線程安全

        懶漢式單例如何保證線程安全呢?通過 synchronized 關(guān)鍵字加鎖保證線程安全,synchronized 可以添加在方法上面,也可以添加在代碼塊上面,這里演示添加在方法上面,存在的問題是每一次調(diào)用 getInstance 獲取實例時都需要加鎖和釋放鎖,這樣是非常影響性能的。
        優(yōu)點:懶加載,線程安全

        缺點:效率較低
        代碼實現(xiàn)如下:
        /**
         * 懶漢式單例,方法上面添加 synchronized 保證線程安全
         *
         * @className: Singleton
         * @date: 2021/6/7 14:32
         */
        public class Singleton  {
            // 1、私有化構(gòu)造方法
            private Singleton(){ }
            // 2、定義一個靜態(tài)變量指向自己類型
            private static Singleton instance;
            // 3、對外提供一個公共的方法獲取實例
            public synchronized static Singleton getInstance() {
                if (instance == null) {
                    instance = new Singleton();
                }
                return instance;
            }
        }

        4、雙重檢查鎖(DCL, 即 double-checked locking)

        實現(xiàn)代碼如下:

        /**
         * 雙重檢查鎖(DCL, 即 double-checked locking)
         *
         * @className: Singleton
         * @date: 2021/6/7 14:32
         */
        public class Singleton {
            // 1、私有化構(gòu)造方法
            private Singleton() {
            }

            // 2、定義一個靜態(tài)變量指向自己類型
            private volatile static Singleton instance;

            // 3、對外提供一個公共的方法獲取實例
            public synchronized static Singleton getInstance() {
                // 第一重檢查是否為 null
                if (instance == null) {
                    // 使用 synchronized 加鎖
                    synchronized (Singleton.class) {
                        // 第二重檢查是否為 null
                        if (instance == null) {
                            // new 關(guān)鍵字創(chuàng)建對象不是原子操作
                            instance = new Singleton();
                        }
                    }
                }
                return instance;
            }
        }
        優(yōu)點:懶加載,線程安全,效率較高

        缺點:實現(xiàn)較復(fù)雜

        這里的雙重檢查是指兩次非空判斷,鎖指的是 synchronized 加鎖,為什么要進(jìn)行雙重判斷,其實很簡單,第一重判斷,如果實例已經(jīng)存在,那么就不再需要進(jìn)行同步操作,而是直接返回這個實例,如果沒有創(chuàng)建,才會進(jìn)入同步塊,同步塊的目的與之前相同,目的是為了防止有多個線程同時調(diào)用時,導(dǎo)致生成多個實例,有了同步塊,每次只能有一個線程調(diào)用訪問同步塊內(nèi)容,當(dāng)?shù)谝粋€搶到鎖的調(diào)用獲取了實例之后,這個實例就會被創(chuàng)建,之后的所有調(diào)用都不會進(jìn)入同步塊,直接在第一重判斷就返回了單例。
        關(guān)于內(nèi)部的第二重空判斷的作用,當(dāng)多個線程一起到達(dá)鎖位置時,進(jìn)行鎖競爭,其中一個線程獲取鎖,如果是第一次進(jìn)入則為 null,會進(jìn)行單例對象的創(chuàng)建,完成后釋放鎖,其他線程獲取鎖后就會被空判斷攔截,直接返回已創(chuàng)建的單例對象。
        其中最關(guān)鍵的一個點就是 volatile 關(guān)鍵字的使用,關(guān)于 volatile 的詳細(xì)介紹可以直接搜索 volatile 關(guān)鍵字即可,有很多寫的非常好的文章,這里不做詳細(xì)介紹,簡單說明一下,雙重檢查鎖中使用 volatile 的兩個重要特性:可見性、禁止指令重排序
        這里為什么要使用 volatile
        這是因為 new 關(guān)鍵字創(chuàng)建對象不是原子操作,創(chuàng)建一個對象會經(jīng)歷下面的步驟:
        1. 在堆內(nèi)存開辟內(nèi)存空間
        2. 調(diào)用構(gòu)造方法,初始化對象
        3. 引用變量指向堆內(nèi)存空間
        對應(yīng)字節(jié)碼指令如下:


        為了提高性能,編譯器和處理器常常會對既定的代碼執(zhí)行順序進(jìn)行指令重排序,從源碼到最終執(zhí)行指令會經(jīng)歷如下流程:


        最終執(zhí)行指令序列

        所以經(jīng)過指令重排序之后,創(chuàng)建對象的執(zhí)行順序可能為 1 2 3 或者 1 3 2 ,因此當(dāng)某個線程在亂序運行 1 3 2 指令的時候,引用變量指向堆內(nèi)存空間,這個對象不為 null,但是沒有初始化,其他線程有可能這個時候進(jìn)入了 getInstance 的第一個 if(instance == null) 判斷不為 nulll ,導(dǎo)致錯誤使用了沒有初始化的非 null 實例,這樣的話就會出現(xiàn)異常,這個就是著名的 DCL 失效問題。
        當(dāng)我們在引用變量上面添加 volatile 關(guān)鍵字以后,會通過在創(chuàng)建對象指令的前后添加內(nèi)存屏障來禁止指令重排序,就可以避免這個問題,而且對 volatile 修飾的變量的修改對其他任何線程都是可見的。

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

        代碼實現(xiàn)如下:
        /**
         * 靜態(tài)內(nèi)部類實現(xiàn)單例
         *
         * @className: Singleton
         * @date: 2021/6/7 14:32
         */
        public class Singleton {
            // 1、私有化構(gòu)造方法
            private Singleton() {
            }
            
            // 2、對外提供獲取實例的公共方法
            public static Singleton getInstance() {
                return InnerClass.INSTANCE;
            }

            // 定義靜態(tài)內(nèi)部類
            private static class InnerClass{
                private final static Singleton INSTANCE = new Singleton();
            }

        }
        優(yōu)點:懶加載,線程安全,效率較高,實現(xiàn)簡單
        靜態(tài)內(nèi)部類單例是如何實現(xiàn)懶加載的呢?首先,我們先了解下類的加載時機(jī)。
        虛擬機(jī)規(guī)范要求有且只有5種情況必須立即對類進(jìn)行初始化(加載、驗證、準(zhǔn)備需要在此之前開始):
        • 遇到 new、getstaticputstatic、invokestatic 這4條字節(jié)碼指令時。生成這4條指令最常見的 Java 代碼場景是:使用 new 關(guān)鍵字實例化對象的時候、讀取或設(shè)置一個類的靜態(tài)字段(final修飾除外,被final修飾的靜態(tài)字段是常量,已在編譯期把結(jié)果放入常量池)的時候,以及調(diào)用一個類的靜態(tài)方法的時候。
        • 使用 java.lang.reflect 包方法對類進(jìn)行反射調(diào)用的時候。
        • 當(dāng)初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化,則需要先觸發(fā)其父類的初始化。
        • 當(dāng)虛擬機(jī)啟動時,用戶需要指定一個要執(zhí)行的主類(包含main()的那個類),虛擬機(jī)會先初始化這個主類。
        • 當(dāng)使用JDK 1.7的動態(tài)語言支持時,如果一個 java.lang.invoke.MethodHandle 實例最后的解析結(jié)果是REF_getStaticREF_putStatic、REF_invokeStatic 的方法句柄,則需要先觸發(fā)這個方法句柄所對應(yīng)的類的初始化。

        這5種情況被稱為是類的主動引用,注意,這里《虛擬機(jī)規(guī)范》中使用的限定詞是 "有且僅有",那么,除此之外的所有引用類都不會對類進(jìn)行初始化,稱為被動引用。靜態(tài)內(nèi)部類就屬于被動引用的情況。
        當(dāng)getInstance()方法被調(diào)用時,InnerClass 才在 Singleton 的運行時常量池里,把符號引用替換為直接引用,這時靜態(tài)對象 INSTANCE 也真正被創(chuàng)建,然后再被 getInstance()方法返回出去,這點同餓漢模式。
        那么 INSTANCE 在創(chuàng)建過程中又是如何保證線程安全的呢?在《深入理解JAVA虛擬機(jī)》中,有這么一句話:
         虛擬機(jī)會保證一個類的 <clinit>() 方法在多線程環(huán)境中被正確地加鎖、同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執(zhí)行這個類的 <clinit>() 方法,其他線程都需要阻塞等待,直到活動線程執(zhí)行 <clinit>() 方法完畢。如果在一個類的 <clinit>() 方法中有耗時很長的操作,就可能造成多個進(jìn)程阻塞(需要注意的是,其他線程雖然會被阻塞,但如果執(zhí)行<clinit>()方法后,其他線程喚醒之后不會再次進(jìn)入<clinit>()方法。同一個加載器下,一個類型只會初始化一次。),在實際應(yīng)用中,這種阻塞往往是很隱蔽的。
        從上面的分析可以看出INSTANCE在創(chuàng)建過程中是線程安全的,所以說靜態(tài)內(nèi)部類形式的單例可保證線程安全,也能保證單例的唯一性,同時也延遲了單例的實例化。

        6、枚舉單例

        代碼實現(xiàn)如下:
        /**
         * 枚舉實現(xiàn)單例
         *
         * @className: Singleton
         * @date: 2021/6/7 14:32
         */
        public enum Singleton {
            INSTANCE;
            public void doSomething(String str) {
                System.out.println(str);
            }
        }
        優(yōu)點:簡單,高效,線程安全,可以避免通過反射破壞枚舉單例
        枚舉在java中與普通類一樣,都能擁有字段與方法,而且枚舉實例創(chuàng)建是線程安全的,在任何情況下,它都是一個單例,可以直接通過如下方式調(diào)用獲取實例:
        Singleton singleton = Singleton.INSTANCE;
        使用下面的命令反編譯枚舉類
        javap Singleton.class
        得到如下內(nèi)容
        Compiled from "Singleton.java"
        public final class com.spring.demo.singleton.Singleton extends java.lang.Enum<com.spring.demo.singleton.Singleton> {
          public static final com.spring.demo.singleton.Singleton INSTANCE;
          public static com.spring.demo.singleton.Singleton[] values();
          public static com.spring.demo.singleton.Singleton valueOf(java.lang.String);
          public void doSomething(java.lang.String);
          static {};
        }
        從枚舉的反編譯結(jié)果可以看到,INSTANCE 被 static final 修飾,所以可以通過類名直接調(diào)用,并且創(chuàng)建對象的實例是在靜態(tài)代碼塊中創(chuàng)建的,因為 static 類型的屬性會在類被加載之后被初始化,當(dāng)一個Java類第一次被真正使用到的時候靜態(tài)資源被初始化、Java類的加載和初始化過程都是線程安全的,所以創(chuàng)建一個enum類型是線程安全的。
        通過反射破壞枚舉,實現(xiàn)代碼如下:
        public class Test {
            public static void main(String[] args) throws Exception {
                Singleton singleton = Singleton.INSTANCE;
                singleton.doSomething("hello enum");

                // 嘗試使用反射破壞單例
                // 枚舉類沒有空參構(gòu)造方法,反編譯后可以看到枚舉有一個兩個參數(shù)的構(gòu)造方法
                Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(String.class, int.class);
                // 設(shè)置強(qiáng)制訪問
                declaredConstructor.setAccessible(true);
                // 創(chuàng)建實例,這里會報錯,因為無法通過反射創(chuàng)建枚舉的實例
                Singleton enumSingleton = declaredConstructor.newInstance();
                System.out.println(enumSingleton);
            }
        }
        運行結(jié)果報如下錯誤:
        Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
            at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:492)
            at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
            at com.spring.demo.singleton.Test.main(Test.java:24)
        查看反射創(chuàng)建實例的 newInstance() 方法,有如下判斷:


        所以無法通過反射創(chuàng)建枚舉的實例。

        六、總結(jié)

        在java中,如果一個Singleton類實現(xiàn)了java.io.Serializable接口,當(dāng)這個singleton被多次序列化然后反序列化時,就會創(chuàng)建多個Singleton類的實例。為了避免這種情況,應(yīng)該實現(xiàn) readResolve 方法。請參閱 javadocs 中的 Serializable () 和 readResolve Method () 。
        public class Singleton implements Serializable {
            // 1、私有化構(gòu)造方法
            private Singleton() {
            }

            // 2、對外提供獲取實例的公共方法
            public static Singleton getInstance() {
                return InnerClass.instance;
            }

            // 定義靜態(tài)內(nèi)部類
            private static class InnerClass{
                private final static Singleton instance = new Singleton();
            }


            // 對象被反序列化之后,這個方法立即被調(diào)用,我們重寫這個方法返回單例對象.
            protected Object readResolve() {
                    return getInstance();
            }
        }
        使用單例設(shè)計模式需要注意的點:
        • 多線程- 在多線程應(yīng)用程序中必須使用單例時,應(yīng)特別小心。

        • 序列化- 當(dāng)單例實現(xiàn) Serializable 接口時,他們必須實現(xiàn) readResolve 方法以避免有 2 個不同的對象。

        • 類加載器- 如果 Singleton 類由 2 個不同的類加載器加載,我們將有 2 個不同的類,每個類加載一個。

        • 由類名表示的全局訪問點- 使用類名獲取單例實例。這是一種訪問它的簡單方法,但它不是很靈活。如果我們需要替換Sigleton類,代碼中的所有引用都應(yīng)該相應(yīng)地改變。


        本文簡單介紹了單例設(shè)計模式的幾種實現(xiàn)方式,除了枚舉單例,其他的所有實現(xiàn)都可以通過反射破壞單例模式,在《effective java》中推薦枚舉實現(xiàn)單例模式,在實際場景中使用哪一種單例實現(xiàn),需要根據(jù)自己的情況選擇,適合當(dāng)前場景的才是比較好的方式。


        點擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開更多互動和交流,掃描下方”二維碼“或在“公眾號后臺回復(fù)“ 入群 ”即可加入我們的技術(shù)交流群,收獲更多的技術(shù)文章~

        - END -


        瀏覽 42
        點贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
          
          

            1. 国内精品久久久久久2021浪潮 | 中国偷窥妇wc | 美女操比网站 | 91在线无码精品秘 豆花 | 国产精品嫩草影院ccm |