溫馨提示×

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

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

Android設(shè)計(jì)模式之單例模式怎么創(chuàng)建

發(fā)布時(shí)間:2023-04-15 14:45:55 來源:億速云 閱讀:126 作者:iii 欄目:開發(fā)技術(shù)

本篇內(nèi)容介紹了“Android設(shè)計(jì)模式之單例模式怎么創(chuàng)建”的有關(guān)知識(shí),在實(shí)際案例的操作過程中,不少人都會(huì)遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

    一、概念

    單例模式是運(yùn)用最廣泛的設(shè)計(jì)模式之一,在應(yīng)用這個(gè)模式時(shí),單例模式的類必須保證只有一個(gè)實(shí)例存在。多用于整個(gè)程序只需要有一個(gè)實(shí)例,通常很消耗資源的類,比如線程池,緩存,網(wǎng)絡(luò)請(qǐng)求,IO操作,訪問數(shù)據(jù)庫等。由于類比較耗資源,所以沒必要讓它構(gòu)造多個(gè)實(shí)例,這種就是單例模式比較好的使用場(chǎng)景。

    1.1 單例類

    單例模式(Singleton Pattern):一個(gè)類有且僅有一個(gè)實(shí)例,并且自行實(shí)例化向整個(gè)系統(tǒng)提供,也稱為單例類。

    單例模式有三個(gè)要點(diǎn):

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

    2.必須自行創(chuàng)建這個(gè)實(shí)例。

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

    具體實(shí)現(xiàn)角度來說就是以下幾點(diǎn):

    1.單例模式的類只提供私有的構(gòu)造函數(shù)。

    2.通過一個(gè)靜態(tài)方法或者枚舉返回單例類對(duì)象。

    3.確保單例類有且只有一個(gè)靜態(tài)私有對(duì)象,尤其是在多線程環(huán)境下。

    4.提供了一個(gè)靜態(tài)的公有的函數(shù)用于創(chuàng)建或獲取它本身的靜態(tài)私有對(duì)象。

    5.確保單例類對(duì)象在反序列化時(shí)不會(huì)重新構(gòu)建對(duì)象。

    在單例類的內(nèi)部實(shí)現(xiàn)只生成一個(gè)實(shí)例,同時(shí)它提供一個(gè)靜態(tài)的getInstance()工廠方法,讓客戶可以訪問它的唯一實(shí)例;為了防止在外部對(duì)其實(shí)例化,將其構(gòu)造函數(shù)設(shè)計(jì)為私有;在單例類內(nèi)部定義了一個(gè)Singleton類型的靜態(tài)對(duì)象,作為外部共享的唯一實(shí)例。

    1.2 優(yōu)缺點(diǎn)

    1.2.1 優(yōu)點(diǎn)

    1.單例模式提供了對(duì)唯一實(shí)例的受控訪問。因?yàn)閱卫惙庋b了它的唯一實(shí)例,所以它可以嚴(yán)格控制客戶怎樣以及何時(shí)訪問它。

    2.由于在系統(tǒng)內(nèi)存中只存在一個(gè)對(duì)象,因此可以節(jié)約系統(tǒng)資源,對(duì)于一些需要頻繁創(chuàng)建和銷毀的對(duì)象單例模式無疑可以提高系統(tǒng)的性能。

    3.允許可變數(shù)目的實(shí)例?;趩卫J轿覀兛梢赃M(jìn)行擴(kuò)展,使用與單例控制相似的方法來獲得指定個(gè)數(shù)的對(duì)象實(shí)例,既節(jié)省系統(tǒng)資源,又解決了單例單例對(duì)象共享過多有損性能的問題。

    1.2.2 缺點(diǎn)

    1.由于單例模式中沒有抽象層,因此單例類的擴(kuò)展有很大的困難。

    2.單例類的職責(zé)過重,在一定程度上違背了“單一職責(zé)原則”。因?yàn)閱卫惣瘸洚?dāng)了工廠角色,提供了工廠方法,同時(shí)又充當(dāng)了產(chǎn)品角色,包含一些業(yè)務(wù)方法,將產(chǎn)品的創(chuàng)建和產(chǎn)品的本身的功能融合到一起。

    3.現(xiàn)在很多面向?qū)ο笳Z言(如Java、C#)的運(yùn)行環(huán)境都提供了自動(dòng)垃圾回收的技術(shù),因此,如果實(shí)例化的共享對(duì)象長時(shí)間不被利用,系統(tǒng)會(huì)認(rèn)為它是垃圾,會(huì)自動(dòng)銷毀并回收資源,下次利用時(shí)又將重新實(shí)例化,這將導(dǎo)致共享的單例對(duì)象狀態(tài)的丟失。

    二、創(chuàng)建單例模式的方法

    2.1 餓漢式

    強(qiáng)調(diào)餓,那么在創(chuàng)建對(duì)象實(shí)例的時(shí)候就比較著急,餓了嘛,于是在裝載類的時(shí)候就創(chuàng)建對(duì)象實(shí)例。

    這種方法非常簡單,因?yàn)閱卫膶?shí)例被聲明成 static 和 final 變量,在第一次加載類到內(nèi)存中時(shí)就會(huì)初始化,所以創(chuàng)建實(shí)例本身是線程安全的。

    public class SingletonHungry {
        //類加載時(shí)就初始化
        private static final SingletonHungry singleton = new SingletonHungry();
        private SingletonHungry(){}
        public static SingletonHungry getInstance(){
            return singleton;
        }
    }

    缺點(diǎn)是它不是一種懶加載模式,即使客戶端沒有調(diào)用 getInstance()方法,單例會(huì)在加載類后一開始就被初始化。

    餓漢式的創(chuàng)建方式在一些場(chǎng)景中將無法使用:如 Singleton 實(shí)例的創(chuàng)建是依賴參數(shù)或者配置文件的,在 getInstance() 之前必須調(diào)用某個(gè)方法設(shè)置參數(shù)給它,那樣這種單例寫法就無法使用了。

    2.2 懶漢式

    強(qiáng)調(diào)懶,那么在創(chuàng)建對(duì)象實(shí)例的時(shí)候就不著急,什么時(shí)候用什么時(shí)候創(chuàng)建。所以在裝載對(duì)象的時(shí)候不創(chuàng)建對(duì)象實(shí)例。

    2.2.1 懶漢式(非線程安全)
    public class SingletonLazy {
        private static SingletonLazy singletonLazy;
        private SingletonLazy(){}
        public static SingletonLazy getInstance(){
            if (singletonLazy == null) {
                singletonLazy = new SingletonLazy();
            }
            return singletonLazy;
        }
    }

    這段代使用了懶加載模式,但是卻存在致命的問題。當(dāng)有多個(gè)線程并行調(diào)用 getInstance() 的時(shí)候,就會(huì)創(chuàng)建多個(gè)實(shí)例。也就是說在多線程下不能正常工作。那么怎么解決?最簡單的方法是給 getInstance() 方法加個(gè)同步鎖(synchronized)。

    2.2.2 懶漢式(線程安全)
    public class SingletonLazy {
        private static SingletonLazy singletonLazy;
        private SingletonLazy(){}
        public static synchronized SingletonLazy getInstance(){
            if (singletonLazy == null) {
                singletonLazy = new SingletonLazy();
            }
            return singletonLazy;
        }
    }

    上面通過添加 synchronized 關(guān)鍵字,使得getInstance()是一個(gè)同步方法,保證多線程情況下單例對(duì)象的唯一性。

    雖然做到了線程安全,并且解決了多實(shí)例的問題,但是它并不高效。因?yàn)樵谌魏螘r(shí)候只能有一個(gè)線程調(diào)用 getInstance() 方法。但是同步操作只需要在第一次調(diào)用時(shí)才被需要,即第一次創(chuàng)建單例實(shí)例對(duì)象時(shí)。這就引出了雙重檢驗(yàn)鎖。

    2.3 雙重檢驗(yàn)鎖

    雙重檢驗(yàn)鎖模式(double checked locking pattern),是一種使用同步塊加鎖的方法。程序員稱其為雙重檢查鎖,也是網(wǎng)上使用畢竟頻繁的一種方式。

    為什么叫雙重檢查鎖?因?yàn)闀?huì)有兩次檢查 instance == null,一次是在同步塊外,一次是在同步塊內(nèi)。為什么在同步塊內(nèi)還要再檢驗(yàn)一次?因?yàn)榭赡軙?huì)有多個(gè)線程一起進(jìn)入同步塊外的 if,避免不必要的同步。如果在同步塊內(nèi)不進(jìn)行二次檢驗(yàn)的話就會(huì)生成多個(gè)實(shí)例,避免生成多個(gè)實(shí)例。

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

    這段代碼看起來很完美有著雙重檢查,但很可惜,它是有問題。主要在于 singleton = new SingletonDCL()。事實(shí)上在 JVM 中這句話大概做了下面 3 件事情:

    1.給 singleton 分配內(nèi)存。

    2.調(diào)用 SingletonDCL 的構(gòu)造函數(shù)來初始化成員變量。

    3.將singleton對(duì)象指向分配的內(nèi)存空間(執(zhí)行完這步 singleton 就為非 null)。

    但是在 JVM 的即時(shí)編譯器中存在指令重排序的優(yōu)化。也就是說上面的第2步和第3步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執(zhí)行完畢、但 2 未執(zhí)行之前,被線程二搶占了,這時(shí) singleton 已經(jīng)是非 null 了(但卻沒有初始化),所以線程二會(huì)直接返回 singleton(第2步未執(zhí)行),然后使用,然后順理成章地報(bào)錯(cuò)。

    我們只需要將 singleton 變量聲明成 volatile 就可以了。

    public class SingletonDCL {
        private volatile static SingletonDCL singleton;//變量聲明成volatile
        private SingletonDCL(){}
        public static SingletonDCL getInstance(){
            if (singleton == null) {
                synchronized (SingletonDCL.class){
                    if (singleton == null) {
                        singleton = new SingletonDCL();
                    }
                }
            }
            return singleton;
        }
    }

    使用 volatile 的主要原因是其有一個(gè)特性:禁止指令重排序優(yōu)化。也就是說,在 volatile 變量的賦值操作后面會(huì)有一個(gè)內(nèi)存屏障(生成的匯編代碼上),讀操作不會(huì)被重排序到內(nèi)存屏障之前。比如上面的例子,取操作必須在執(zhí)行完 1-2-3 之后或者 1-3-2 之后,不存在執(zhí)行到 1-3 然后取到值的情況。

    當(dāng)然 volatile 變量還有一個(gè)規(guī)則:對(duì)一個(gè)變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作(這里的"后面"是時(shí)間上的先后順序)。

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

    public class SingletonNested {
        //靜態(tài)內(nèi)部類
        private static class SingletonHolder{
            private static final SingletonNested singleton = new SingletonNested();
        }
        private SingletonNested(){}
        public static SingletonNested getInstance(){
            return SingletonHolder.singleton;
        }
    }

    使用JVM本身機(jī)制保證了線程安全問題。由于靜態(tài)單例對(duì)象沒有作為Singleton的成員變量直接實(shí)例化,因此類加載時(shí)不會(huì)實(shí)例化SingletonNested,第一次調(diào)用getInstance()時(shí)將加載內(nèi)部類SingletonHolder,在該內(nèi)部類中定義了一個(gè)static類型的變量singleton,此時(shí)會(huì)首先初始化這個(gè)成員變量,由Java虛擬機(jī)來保證其線程安全性,確保該成員變量只能初始化一次。由于getInstance()方法沒有任何線程鎖定,因此其性能不會(huì)造成任何影響。 s

    由于 SingletonHolder 是私有的,除了 getInstance() 之外沒有辦法訪問它,因此它是懶漢式的,同時(shí)讀取實(shí)例的時(shí)候不會(huì)進(jìn)行同步,沒有性能缺陷,也不依賴 JDK 版本。

    2.5 枚舉

    public enum SingletonEnum {
        SINGLETON;
        public void doSomeThing() {
        }
    }

    我們可以通過SingletonEnum.SINGLETON來訪問實(shí)例,這比調(diào)用getInstance()方法簡單多了。創(chuàng)建枚舉默認(rèn)就是線程安全的,所以不需要擔(dān)心double checked locking,而且還能防止反序列化導(dǎo)致重新創(chuàng)建新的對(duì)象。

    小結(jié)

    單例模式不管用那種方式實(shí)現(xiàn),核心思想都相同:

    1.構(gòu)造函數(shù)私有化,通過一次靜態(tài)方法獲取一個(gè)唯一實(shí)例

    2.線程安全

    使用場(chǎng)景:

    1.需要頻繁的進(jìn)行創(chuàng)建和銷毀的對(duì)象。

    2.創(chuàng)建對(duì)象時(shí)耗時(shí)過多或耗費(fèi)資源過多,但又經(jīng)常用到的對(duì)象。

    3.工具類對(duì)象。

    4.頻繁訪問數(shù)據(jù)庫或文件的對(duì)象。

    一般情況下直接使用餓漢式就好了,當(dāng)然推薦使用文中DCL方式和靜態(tài)內(nèi)部類的方式來創(chuàng)建單例模式。如果涉及到反序列化創(chuàng)建對(duì)象時(shí)會(huì)試著使用枚舉的方式來實(shí)現(xiàn)單例。當(dāng)然,枚舉單例的優(yōu)點(diǎn)就是簡單,但是大部分應(yīng)用開發(fā)很少用枚舉,可讀性并不是很高,不建議用。

    三、擴(kuò)展

    3.1 防止反序列化

    上文使用枚舉可以防止反序列化導(dǎo)致重新創(chuàng)建新的對(duì)象。那么其他幾種實(shí)現(xiàn)單例模式的方式怎么方式防止反序列化導(dǎo)致重新創(chuàng)建新的對(duì)象?那就是反序列化??梢詤⒖夹蛄谢晃?。

    反序列化操作提供了一個(gè)很特別的鉤子函數(shù),類中具有一個(gè)私有的readResolve()函數(shù),這個(gè)函數(shù)可以讓開發(fā)人員控制對(duì)象的反序列化。

    public class SingletonDCL implements Serializable {
        private volatile static SingletonDCL singleton;//變量聲明成volatile
        ...
        private Object readResolve() throws ObjectStreamException {
            return singleton;
        }
    }

    在readResolve方法中將單例對(duì)象返回,而不是重新生成一個(gè)新對(duì)象。

    3.2 volatile 關(guān)鍵字

    Java內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存中。每條線程中還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了被該線程所使用到的變量(這些變量是從主內(nèi)存中拷貝而來)。線程對(duì)變量的所有操作(讀取,賦值)都必須在工作內(nèi)存中進(jìn)行。不同線程之間也無法直接訪問對(duì)方工作內(nèi)存中的變量,線程間變量值的傳遞均需要通過主內(nèi)存來完成。

    作用

    1.線程可見性

    當(dāng)一個(gè)共享變量被volatile修飾時(shí),它會(huì)保證修改的值會(huì)立即被更新到主存,當(dāng)有其他線程需要讀取時(shí),它會(huì)去內(nèi)存中讀取新值。

    2.指令重排序

    沒加之前,指令是并發(fā)執(zhí)行的,第一個(gè)線程執(zhí)行到一半另一個(gè)線程可能開始執(zhí)行了。加了volatile關(guān)鍵字后,不同線程是按照順序一步一步執(zhí)行的。例如上面2.3 雙重檢驗(yàn)鎖。

    “Android設(shè)計(jì)模式之單例模式怎么創(chuàng)建”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

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

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

    AI