溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

分析Java中的單例模式

發(fā)布時間:2021-11-03 13:32:16 來源:億速云 閱讀:99 作者:iii 欄目:開發(fā)技術(shù)

本篇內(nèi)容主要講解“分析Java中的單例模式”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“分析Java中的單例模式”吧!

    WHAT

    維基百科給出了解釋、實(shí)現(xiàn)的思路以及應(yīng)該注意的地方:

    單例模式,也叫單子模式,是一種常用的軟件設(shè)計模式,屬于創(chuàng)建型模式的一種。在應(yīng)用這個模式時,單例對象的類必須保證只有一個實(shí)例存在。
    實(shí)現(xiàn)單例模式的思路是:一個類能返回對象一個引用(永遠(yuǎn)是同一個)和一個獲得該實(shí)例的方法(必須是靜態(tài)方法,通常使用getInstance這個名稱);當(dāng)我們調(diào)用這個方法時,如果類持有的引用不為空就返回這個引用,如果類保持的引用為空就創(chuàng)建該類的實(shí)例并將實(shí)例的引用賦予該類保持的引用;同時我們還將該類的構(gòu)造函數(shù)定義為私有方法,這樣其他處的代碼就無法通過調(diào)用該類的構(gòu)造函數(shù)來實(shí)例化該類的對象,只有通過該類提供的靜態(tài)方法來得到該類的唯一實(shí)例。
    單例模式在多線程的應(yīng)用場合下必須小心使用。如果當(dāng)唯一實(shí)例尚未創(chuàng)建時,有兩個線程同時調(diào)用創(chuàng)建方法,那么它們同時沒有檢測到唯一實(shí)例的存在,從而同時各自創(chuàng)建了一個實(shí)例,這樣就有兩個實(shí)例被構(gòu)造出來,從而違反了單例模式中實(shí)例唯一的原則。解決這個問題的辦法是為指示類是否已經(jīng)實(shí)例化的變量提供一個互斥鎖(雖然這樣會降低效率)。

    類圖是:

    分析Java中的單例模式

    WHY

    正如定義所說,單例模式就是整個內(nèi)存模型中,只有一個實(shí)例。實(shí)例少了,內(nèi)存占用就少。同時,只有一個實(shí)例,也就只需要構(gòu)建一個對象,計算就少。對于構(gòu)造過程中需要大量計算或者占用大量資源的對象,只創(chuàng)建一次,就減少了資源占用和內(nèi)存占用。

    餓漢式

    餓漢式是最簡單的一種實(shí)現(xiàn),在類裝載過程中,完成實(shí)例化,避免多線程問題。

    實(shí)現(xiàn)一:靜態(tài)實(shí)例參數(shù)與靜態(tài)代碼塊

    public class EagerSingleton {
        private static final EagerSingleton INSTANCE = new EagerSingleton();
    
        private EagerSingleton() {
        }
    
        public static EagerSingleton getInstance() {
            return INSTANCE;
        }
    }

    根據(jù)java的特性,餓漢式還可以變種寫法,有的地方稱為靜態(tài)代碼塊方式:

    public class EagerSingleton {
        private static EagerSingleton INSTANCE = null;
    
        static {
            INSTANCE = new EagerSingleton();
        }
    
        private EagerSingleton() {
        }
    
        public static EagerSingleton getInstance() {
            return INSTANCE;
        }
    }

    這兩種方式只是在寫法上的區(qū)別,優(yōu)缺點(diǎn)沒有區(qū)別,只是借助Java語言特性的不同寫法,所以歸為一類。

    餓漢式有兩個明顯的缺點(diǎn):

    1. 類裝載過程即完成實(shí)例化,如果整個應(yīng)用生命周期內(nèi),實(shí)例沒有使用,也就是浪費(fèi)資源了。

    2. 因?yàn)闆]有辦法向構(gòu)造函數(shù)傳遞不同的參數(shù),如果需要通過個性化參數(shù)定制實(shí)例時,這種方式就不支持了。

    實(shí)現(xiàn)二:靜態(tài)內(nèi)部類

    針對餓漢式第一個缺點(diǎn),我們可以借助靜態(tài)內(nèi)部類的方式,將對象實(shí)例化的時間延后。

    public class EagerSingleton {
        private EagerSingleton() {
        }
    
        private static class EagerSingletonInstance {
            private static final EagerSingleton INSTANCE = new EagerSingleton();
        }
    
        public static EagerSingleton getInstance() {
            return EagerSingletonInstance.INSTANCE;
        }
    }

    但是,依然不能很好的解決第二個缺點(diǎn),如果需要根據(jù)不同的參數(shù)實(shí)現(xiàn)不同的實(shí)例,可以采用下面說的懶漢式實(shí)現(xiàn)。

    懶漢式

    懶漢式比餓漢式的一個優(yōu)點(diǎn),就是能夠在使用的時候再進(jìn)行實(shí)例化。但是,餡餅總是要伴隨著陷阱,懶漢式寫法有更多的坑,一不小心就摔著了。

    錯誤一:單線程實(shí)現(xiàn)

    public class LazySingleton {
        private static LazySingleton INSTANCE = null;
    
        private LazySingleton() {
        }
    
        public static LazySingleton getInstance() {
            if (INSTANCE == null) {
                INSTANCE = new LazySingleton();
            }
            return INSTANCE;
        }
    }

    之所以定義為單線程實(shí)現(xiàn),是因?yàn)?INSTANCE==null這個判斷,一個線程通過這個判斷,開始進(jìn)行對象實(shí)例化,但是還沒有實(shí)例化完成,另一個線程又來了,這個時候,對象還沒有實(shí)例化,就也會開始進(jìn)行實(shí)例化,造成不必要的浪費(fèi)。

    錯誤二:同步方法

    public class LazySingleton {
        private static LazySingleton INSTANCE = null;
    
        private LazySingleton() {
        }
    
        public static synchronized LazySingleton getInstance() {
            if (INSTANCE == null) {
                INSTANCE = new LazySingleton();
            }
            return INSTANCE;
        }
    }

    這種方式解決了多線程的問題,但是也引入了新的性能問題:太慢。synchronized把整個方法包起來,也就是每個線程進(jìn)入的時候,都需要等待其他線程結(jié)束調(diào)用,才能拿到實(shí)例,在性能敏感的場景,是比較致命的。

    錯誤三:同步代碼塊之單次檢查

    public class LazySingleton {
        private static LazySingleton INSTANCE = null;
    
        private LazySingleton() {
        }
    
        public static LazySingleton getInstance() {
            if (INSTANCE == null) {
                synchronized (LazySingleton.class) {
                    INSTANCE = new LazySingleton();
                }
            }
            return INSTANCE;
        }
    }

    這種寫法看似將同步代碼縮小,但也縮小了多線程保障,也犯了第一種寫法的錯誤,屬于沒有對多線程有基本了解寫出的低級錯誤代碼。

    錯誤四:同步代碼塊之雙重檢查

    public class LazySingleton {
        private static LazySingleton INSTANCE = null;
    
        private LazySingleton() {
        }
    
        public static LazySingleton getInstance() {
            if (INSTANCE == null) {
                synchronized (LazySingleton.class) {
                    if (INSTANCE == null) {
                        INSTANCE = new LazySingleton();
                    }
                }
            }
            return INSTANCE;
        }
    }

    這種寫法在一定程度上屬于正確的寫法,雙重判斷可以很好的實(shí)現(xiàn)線程安全和延遲加載。如果到這里就結(jié)束,那就是謬以千里的毫厘之差。

    雙重檢查和同步代碼塊都沒有問題,問題出在 INSTANCE=newLazySingleton()這句話。在JVM中,為了充分利用CPU計算能力,會進(jìn)行重排序優(yōu)化, INSTANCE=newLazySingleton()做了三件事:

    1. 為 INSTANCE 初始化??臻g

    2. 為 LazySingleton 分配內(nèi)存空間,實(shí)例化對象

    3. INSTANCE 指向 LazySingleton 實(shí)例分配的內(nèi)存空間

    因?yàn)橹嘏判騼?yōu)化的存在,真正執(zhí)行的過程中,可能會出現(xiàn)1-2-3的順序,也可能出現(xiàn)1-3-2的順序。如果是1-3-2,INSTANCE 指向了 LazySingleton 實(shí)例分配的內(nèi)存空間后,就不是null,另外一個線程進(jìn)入判斷null時,就會直接返回 INSTANCE,但是這個時候 LazySingleton 實(shí)例化還沒有完成,就可能出現(xiàn)意想不到的異常。

    正確:雙重檢查+阻止重排序

    public class LazySingleton {
        private static volatile LazySingleton INSTANCE = null;
    
        private LazySingleton() {
        }
    
        public static LazySingleton getInstance() {
            if (INSTANCE == null) {
                synchronized (LazySingleton.class) {
                    if (INSTANCE == null) {
                        INSTANCE = new LazySingleton();
                    }
                }
            }
            return INSTANCE;
        }
    }

    這種寫法比上面那種,就差在 volatile這個關(guān)鍵字。

    枚舉

    懶漢式和餓漢式都能夠適用于多線程并發(fā)場景,但是通過反序列化或反射可以實(shí)例化對象,這樣依然不能滿足單例模式的要求,所以可以借助枚舉實(shí)現(xiàn),枚舉可以完美避免多線程并發(fā)問題,而且可以防止反序列化和反射創(chuàng)建新對象。第一次看到這樣定義單例模式,是在《Effective Java》中,多讀經(jīng)典書還是挺好的。

    public enum EnumSingleton {
        INSTANCE;
    
        public void method1() {
            // do something
        }
    
        public Object method2() {
            // do something and return something else
            return new Object();
        }
    }

    在開發(fā)實(shí)踐中,枚舉可以滿足絕大部分場景,而且寫法簡單,定義單例的邏輯只需要三行代碼,簡潔而不簡單,三行代碼可以保證線程安全。同時枚舉的反序列化只是通過name查找對象,不會產(chǎn)生新的對象;根據(jù)JVM規(guī)范,通過反射創(chuàng)建枚舉對象時,會拋出 IllegalArgumentException異常。這樣,相當(dāng)于通過語法糖防止反序列化和反射破壞單例。

    場景

    1. 無狀態(tài)工具類:這種工具類不需要記錄狀態(tài),只保證正確的應(yīng)用就行,可以通過單例模式來定義。

    2. 數(shù)據(jù)共享:即多個不相關(guān)的兩個線程或者進(jìn)程之間實(shí)現(xiàn)通信。因?yàn)槭且粋€實(shí)例,如果它的屬性或者變量值被修改,所有引用都是同時修改的,當(dāng)然需要 volatile 來定義變量。比如網(wǎng)站的計數(shù)器。

    3. 日志應(yīng)用:通常應(yīng)用會向日志文件寫日志信息,為了實(shí)時向文件寫,通常會使用單例模式,保證有一個實(shí)例持有文件,然后進(jìn)行操作。

    4. 數(shù)據(jù)庫連接池:數(shù)據(jù)庫連接是一種數(shù)據(jù)庫資源,使用數(shù)據(jù)庫連接池,主要是節(jié)省打開或者關(guān)閉數(shù)據(jù)庫連接所引起的效率損耗,這種效率上的損耗還是非常昂貴的,通過單例模式來維護(hù),就可以大大降低這種損耗。

    5. Web應(yīng)用的配置對象:讀取文件需要消耗時間,如果讀取大文件,消耗的時間和資源更久,所以通過單例模式可以大大降低消耗。

    6. 。。。

    單例模式的場景還是比較多的,這里只是列出里幾個簡單的應(yīng)用場景,算是拋磚引玉,如果看官們有什么其他應(yīng)用場景,可以在說一說

    到此,相信大家對“分析Java中的單例模式”有了更深的了解,不妨來實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI