Android設(shè)計模式—單例模式

定義

確保某個類只有一個實例,而且自行實例化并向整個系統(tǒng)提供這個實例。

應(yīng)用場景

確保某個類有且只有一個對象的場景,避免產(chǎn)生多個對象消耗過多資源,或者某種類型的對象只應(yīng)該有且只有一個。例如,創(chuàng)建一個對象需要消耗的資源過多,如果要訪問IO和數(shù)據(jù)庫等資源,這時就要考慮使用單例模式。

關(guān)鍵點

  • 構(gòu)造函數(shù)不對外開放。
  • 通過靜態(tài)方法或枚舉返回單例類對象。
  • 確保單例類的對象有且只有一個,特別是在多線程環(huán)境下。
  • 確保單例類的對象在反序列化時不會重新構(gòu)造對象。

單例模式實現(xiàn)方式

實現(xiàn)方式比較多,但是都有各自的優(yōu)缺點。

餓漢模式

餓漢模式是在類加載時就創(chuàng)建好單例類實例。

實現(xiàn)

public class HungerSingleton {
    private static HungerSingleton mInstance = new HungerSingleton();

    private HungerSingleton() {
    }

    public static HungerSingleton getInstance() {
        return mInstance;
    }
}

instance是靜態(tài)對象,在聲明的時候就已經(jīng)初始化了,保證了對象的唯一性。

優(yōu)點:

  • 簡單明了,不需要關(guān)心線程同步的問題。
  • 獲取對象比較快,不需要做其他任何工作。

缺點:

  • 類加載時因為需要初始化對象,所以比較慢。
  • 可能會產(chǎn)生很多多余無用的對象。

適用場景

如果單例模式實例在系統(tǒng)中經(jīng)常會被用到,選擇此方式實現(xiàn)單例比較合適。但是如果對象初始化工作復(fù)雜且在程序運(yùn)行過程中不一定會使用到此單例對象,那餓漢模式就不太適合了。

懶漢模式

懶漢模式不同于餓漢模式的是,它是在第一次調(diào)用getInstance()時才初始化單例對象。

實現(xiàn)

public class LazySingleton {
    private static LazySingleton mInstance;

    private LazySingleton() {
    }

    public static synchronized LazySingleton getInstance() {
        if (mInstance == null) {
            mInstance = new LazySingleton();
        }
        return mInstance;
    }
}

給getInstance()加了synchronized關(guān)鍵字,這樣就能夠保證不會有兩個線程同時去訪問這個方法創(chuàng)建重復(fù)對象了,將getInstance()變?yōu)橥椒椒ūWC了單例對象的唯一性。但是在初始化之后獲取對象時都不可避免的會造成同步開銷。

優(yōu)點

  • 節(jié)約資源,在使用到時才會初始化單例對象。

缺點

  • 第一次調(diào)用反應(yīng)稍慢,需要加載的時間。
  • 每次調(diào)用都會進(jìn)行同步,造成不必要的同步開銷。

DCL模式

DCL優(yōu)點是既能在使用使才初始化對象,又能保證線程安全,且在初始化之后再調(diào)用getInstance()不進(jìn)行同步鎖。

實現(xiàn)

public class DCLSingleton {
    private static DCLSingleton mInstance = null;

    private DCLSingleton() {
    }

    public static DCLSingleton getInstance() {
        if (mInstance == null) {
            synchronized (DCLSingleton.class) {
                if (mInstance == null) {
                    mInstance = new DCLSingleton();
                }
            }
        }
        return mInstance;
    }
}

外層對instance的判空避免了多余的同步工作,只有在初始化對象時才需要同步。

內(nèi)層的instance的判空保證了對象的唯一。

DCL失效問題

這種方法暫時還是不安全的,并不能保證一定單例,還是有幾率會失敗的。
主要原因是new一個對象這個操作并不是原子性的,它會被編譯為三條匯編指令,分為三個步驟。但是由于JVM的指令重排,這三個步驟的后兩個步驟順序不定。

  1. 分配內(nèi)存空間。
  2. 初始化內(nèi)存空間。
  3. 引用變量指向這塊內(nèi)存空間。

指令重排序是JVM為了優(yōu)化指令,提高程序運(yùn)行效率。指令重排序包括編譯器重排序和運(yùn)行時重排序。JVM規(guī)范規(guī)定,指令重排序可以在不影響單線程程序執(zhí)行結(jié)果前提下進(jìn)行。

指令重排并沒有考慮多線程的情況,所以可能會出現(xiàn)下面的場景。

Thread 1 Thread 2
外層instance判空:yes -
進(jìn)入同步代碼塊 -
內(nèi)層instance判空:yes -
為單例對象分配內(nèi)存空間 -
instance指向這塊內(nèi)存空間 -
- 外層instance判空:no
- 直接返回未初始化完畢的instance對象
初始化這塊內(nèi)存空間 -

這樣就會拿到一個未初始化完成的對象了,這就是DCL失效問題。

解決失效問題

在java5之后,具體化了volatile關(guān)鍵字,所以在java5之后,就可以用這個關(guān)鍵字來解決DCL失效問題了。

//使用 volatile修飾 instance
private volatile static DCLSingleton mInstance = null;

volatile防止指令重排序,禁止把new過程的指令與把引用賦值給變量的語句重排序,賦值只發(fā)生在new結(jié)束之后。這樣就不會出現(xiàn)上述的失效問題了。

volatile或多或少會影響性能,但是保證了DCL的正確性。

優(yōu)點

  • 節(jié)約資源,在使用到時才會初始化單例對象。

缺點

  • 第一次調(diào)用反應(yīng)稍慢,需要加載的時間。
  • 會有一定的機(jī)率發(fā)生問題。在jdk5之后可以使用volatile保證正確性,volatile或多或少會影響性能。

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

同樣也是在第一次調(diào)用getInstance()才會初始化單例對象。

實現(xiàn)

第一次調(diào)用getInstance()時會導(dǎo)致虛擬機(jī)加載SingletonHolder類,初始化單例對象,這種方法不僅保證了線程安全,單例對象的唯一性,也延遲了單例對象的實例化,實現(xiàn)起來還非常簡單。

public class StaticInnerSingleton {
    private StaticInnerSingleton() {
    }

    public static StaticInnerSingleton getInstance() {
        return SingletonHolder.mInstance;
    }

    private static class SingletonHolder {
        private static final StaticInnerSingleton mInstance = new StaticInnerSingleton();
    }
}

在類的初始化期間,JVM會去獲取一個鎖,這個鎖可以同步多個線程對同一個類的初始化,從而保證了單例唯一。

枚舉單例

這可能是最簡單的單例實現(xiàn)了。枚舉和普通類一樣可以擁有字段和方法,并且默認(rèn)枚舉實例創(chuàng)建是線程安全的,在仍和時刻都是單例的,并且哪怕是支持反序列化,也不會生成新的單例對象。反序列化不同于普通類,枚舉是根據(jù)名字去查找存在的對象,而不是重新創(chuàng)建對象。

實現(xiàn)

public enum EnumSingleton {
    INSTANCE;
    public int a = 2;

    public int aaa() {
        return a;
    }
}

枚舉在序列化的時候Java僅僅是將枚舉對象的name屬性輸出到結(jié)果中,反序列化的時候則是通****過java.lang.Enum的valueOf方法來根據(jù)名字查找枚舉對象。

支持反序列化

而上面幾種方法,如果需要保證完全的單例,還需要去增加readResolve(),需要做如下修改,拿支持序列化的餓漢模式舉例:

public class HungerSingleton implements Serializable {
    private static final long serialVersionUID = 1L;
    //...
    private Object readResolve() throws ObjectStreamException {
        return mInstance;
    }

直接將單例對象返回而不是重新創(chuàng)建新對象。

集合實現(xiàn)

實現(xiàn)

通過一個map來保存所有的單例對象,這種方式換了一種思路,從單例自身轉(zhuǎn)移到單例保存方式上,提供get()、register()來提供單例和獲取單例。

public class MapSingletonManager {
    private static Map<String, Object> objMap = new HashMap<>();

    private MapSingletonManager() {
    }

    public static void registerService(String key, Object instance) {
        if (!objMap.containsKey(key)) {
            objMap.put(key, instance);
        }
    }

    public static Object getService(String key) {
        return objMap.get(key);
    }
}

這種方法降低了耦合,不會像之前的方法一樣將單例邏輯糅雜在類中。但是這種方式需要自己去創(chuàng)建單例對象,并且并不能保證堆中只有一個單例對象,只能保證在使用時,從map中提取出來的是同一個單例對象。

小結(jié)

書上推薦DCL和靜態(tài)內(nèi)部類的方式實現(xiàn)單例,最后總結(jié)一下單例模式的優(yōu)缺點。

優(yōu)點

  • 減少了內(nèi)存開銷。
  • 降低了系統(tǒng)性能開銷。
  • 避免對資源的多重占用。
  • 可以設(shè)置全局訪問點,優(yōu)化和共享資源訪問。

缺點

  • 拓展困難,一般沒有接口。
  • 單例對象如果持有Context,就容易發(fā)生內(nèi)存泄漏。

如果隨便地傳入一個Activity的Context,那么無論這個Activity是否還需要,它都因為Context被單例對象持有,所以Activity無法回收,只要項目活著,這個Activity就活著,所以就造成了內(nèi)存泄漏。所以如果單例中需要Context,就最好傳遞Application的Context,因為Application的生命周期和應(yīng)用一樣長。

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

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

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