一、設計模式之單例模式

一、介紹:

單例模式是應用最廣的模式之一;在應用這個模式時,單例對象的類必須保證只有一個實例的存在;許多時候,整個系統(tǒng)只需要擁有一個全局對象,這樣有利于我們協(xié)調系統(tǒng)整體的行為;如在一個應用中,應該只有一個ImageLoader實例,這個ImageLoder中有含有線程池、緩存系統(tǒng)、網(wǎng)絡請求等,很消耗資源,很消耗資源,因此,沒有理由讓他構造多個實例。這種不能自由構造對象的情況,其實就是單例模式的使用場景;

二、定義

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

三、單例模式使用的場景

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

四、單例模式的關鍵點

1、類的構造函數(shù)不對外開放,一般為Private;
2、通過一個靜態(tài)方法或者枚舉返回單例類對象;
3、確保單例類對象有且只有一個,特別是在多線程的環(huán)境下;
4、確保單例類對象在反序列化時不會重新構建對象;

單例模式的七種寫法

  • 餓漢式(線程安全)

類加載的時候就進行了初始化,容易浪費內存,它基于classloader 機制避免了多線程的同步問題!非懶加載

public class Singleton implements Serializable {

    private static Singleton instance = new Singleton() ;

    private Singleton(){}

    public static Singleton getInstance(){
        return instance ;
    }
    //防止單例對象在反序列化時重新生成對象
    private Object readResolve() throws ObjectStreamException {
        return instance ;
    }
}
object Singleton : Serializable  {
    fun doSomething(){
        println("do something")
    }
    //防止單例對象在反序列化時重新生成對象
    private fun readResolve():Any{
        return Singleton
    }
}
  • 懶漢式(線程不安全)

最簡單的單例實現(xiàn),為懶加載實現(xiàn), 但不支持多線程,容易造成線程不安全。因為沒有加鎖,嚴格來說不算單例!

/**
 * 懶漢式 線程不安全
*/    
public class Singleton {
    private static Singleton instance ;
    
    private Singleton(){}
    
    public static Singleton getInstance(){
        if (instance == null){
            instance = new Singleton();
        }
        return instance ;
    }
}
//懶漢式: 線程不安全
class Singleton private constructor() {
    companion object{
        private var mInstance : Singleton? = null
        get() {
            return field?: Singleton()
        }
        @JvmStatic
        fun getInstance() : Singleton{
            return requireNotNull(mInstance)
        }
    }
    fun doSomething(){
        println("do something")
    }
}
  • 懶漢式(方法加鎖,線程安全)

在上一種實現(xiàn)方式上,在獲取單例的方法上加鎖 synchronized關鍵字,保證單例的實現(xiàn),是懶加載實現(xiàn),能夠很好的在多線程中工作,第一次調用才初始化,避免內存浪費,但是效率很低,因為方法加鎖會影響效率!

/**
 * 懶漢式 方法加鎖 線程安全
*/
public class Singleton {
    private static Singleton instance ;

    private Singleton(){}

    public static synchronized Singleton getInstance(){
        if (instance == null){
            instance = new Singleton();
        }
        return instance ;
    }
}
//懶漢式,方法加鎖的懶漢式,線程安全
class Singleton private constructor() : Serializable {
    companion object {
        private var mInstance : Singleton ? = null
        get() {
            return field?: Singleton()
        }
        @JvmStatic
        @Synchronized       //添加同步鎖
        fun getInstance() : Singleton {
            return requireNotNull(mInstance)
        }
    }
    //防止單例對象在反序列化時生成新的對象
    private fun readResolve():Any{
        return Singleton.getInstance()
    }
    
    fun doSomething(){
        println("do something")
    }
    //kotlin調用
    fun test(){
        Singleton.getInstance().doSomething()
    }
}
  • 懶漢式(雙重校驗鎖,DCL double-check locking ,線程安全)

懶加載 ,采用雙鎖檢查機制,避免在對象實例時,對象實例指令發(fā)生重排,造成對象空指針。在多線程下保存高性能,單例對象需要使用volatile關鍵字聲明,volatile關鍵字是線程同步的輕量級實現(xiàn),能保證數(shù)據(jù)的可見性,但不能保證數(shù)據(jù)的原子性。可在實例域需要延遲化使用。

/**
 *  懶漢式 DCL 線程安全
 */    
public class Singleton {
        //volatile 修飾變量,防止指令重排
    private static volatile Singleton instance ;

    private Singleton(){}

    public static  Singleton getInstance(){
        if (instance == null){
            synchronized (Singleton.class){
                if (instance == null){
                    instance = new Singleton();         
                }
            }
        }
        return instance ;
    }
}
//懶漢式: DCL 線程安全
class Singleton private constructor(){
    companion object{
        //使用lazy屬性代理,并指定LazyThreadSafetyMode為synchronized模式保證線程安全
        @JvmStatic
        val instance : Singleton by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
            Singleton()
        }
    }
    fun doSomething(){
        println("do something")
    }
}
//調用
fun test(){
    Singleton.instance.doSomething()
}

  • 靜態(tài)內部類

能達到和DCL 一樣的功效,但實現(xiàn)更簡單。對靜態(tài)域使用延遲初始化,應使用這種方式而不是DCL。此種方式同樣利用classloader機制來保證初始化單例只有一個線程,它和餓漢式不同的是:餓漢式只要類被裝載了,那么instance 就會被實例化,而靜態(tài)內部類實現(xiàn)單例是類被裝載了,但instance不一定被初始化。因為內部類沒有別主動使用,只有通過getInstance()調用時,才會顯示裝載內部類,從而實例instance。 實現(xiàn)時考慮:想讓單例延遲加載,又不希望單例類加載時就實例化。

/**
 * 靜態(tài)內部類
 */
public class Singleton {
    
    private Singleton(){}
    
    private static class SingletonHolder{
        private static final Singleton instance = new Singleton() ;
    }
    
    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}
//靜態(tài)內部類實現(xiàn)單例
class Singleton private constructor() {
    companion object{
        @JvmStatic
        fun getInstance(): Singleton {
            return SingletonHolder.mInstance 
        }
    }
    fun doSomething(){
        println("do something")
    }
    //靜態(tài)內部類
    private object SingletonHolder {
        val mInstance = Singleton()
    }
}
  • CAS 模式

算是 懶漢式加鎖 的一個變種, synchronized 是一種悲觀鎖, 而 CAS 是樂觀鎖,相對較輕,更輕量級。

import java.util.concurrent.atomic.AtomicReference;

/**
 *  CAS模式 : 存在忙等的問題,可能會造成 CPU 資源的浪費
 */
public class Singleton {

    private static AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>() ;

    private Singleton(){}

    public static final Singleton getInstance(){
           while (true){
               Singleton instance = INSTANCE.get() ;
               if (null == instance){
                   INSTANCE.compareAndSet(null,new Singleton());
               }
               return INSTANCE.get() ;
           }
    }
}
  • 枚舉實現(xiàn)

是多線程安全,非懶加載。沒有被廣泛使用。它很簡介,自動支持序列化機制,防止反序列化重新創(chuàng)建新的對象,絕對防止多次實例化

/**
 *  枚舉實現(xiàn)單例
 */
public enum Singleton {
    //定義一個枚舉,代表Singleton的一個實例
    INSTANCE ; 
    
    public void doSomething(){
        System.out.println("do something");
    }
    //假設在外部調用
    void test(){
        Singleton.INSTANCE.doSomething();
    }
}

//枚舉實現(xiàn)單例
enum class Singleton {
    INSTANCE ;
    fun doSomething(){
        println("do something")
    }
}

總結

單例模式是使用頻率很高的模式,但是,由于在客戶端通常沒有高并發(fā)的情況,因此,選擇哪種實現(xiàn)方法并不會有太大的影響,即使如此,出于效率考慮,一般也是使用DCL方式和內部類單例的實現(xiàn)形式;
優(yōu)點
1、單例模式在內存中只有一個實例,減少了內存開支,特別是一個對象需要頻繁的創(chuàng)建和銷毀時,而且創(chuàng)建和銷毀的性能又無法優(yōu)化,單例模式的優(yōu)勢就非常明顯;
2、單例模式只生產一個實例,減少了系統(tǒng)的新能開銷,當一個對象的場所需要較多的資源時,如讀取配置、產生其他依賴對象時,則可以通過在應用啟動時直接產生一個單例對象,然后永久駐留內存的方式來解決;
3、單例模式可以避免對資源的多重占用,例如一個寫文件操作,由于只有一個實例存在內存中,避免對一個資源文件的同時寫操作;
4、單例模式可以在系統(tǒng)設置全局的訪問點,優(yōu)化和共享資源訪問,例如,可以設計一個單例類,負責所有的數(shù)據(jù)表的映射管理;
缺點
1、單例模式一般沒有借口,擴展困難,如要擴展,除了修改代碼基本上沒有第二種途徑可以實現(xiàn);
2、單例對象如果持有Context,那么很容易引發(fā)內存泄漏,此時需要注意傳給到單例對象的Context最好是Application Context ;
3、不利于測試,與單一職責原則有沖突

什么時候使用?

  • 比如生成唯一序列號的環(huán)境
  • 整個項目中需要一個共享的訪問點或共享數(shù)據(jù)
  • 創(chuàng)建一個對象需要消耗的資源過多
  • 需要定義大量的靜態(tài)常量和靜態(tài)方法(如工具類)的環(huán)境

補充

volatile 關鍵字 : 該關鍵字與內存模型有關,需要先了解內存模型
計算機在執(zhí)行程序時,每條指令都是在CPU中執(zhí)行的,而執(zhí)行過程中,需要進行數(shù)據(jù)的讀取和寫入。程序運行時的臨時數(shù)據(jù)是存放在主存中(物理內存中),這就存在一個問題: 由于CPU執(zhí)行速度很快,而從內存讀取和寫入數(shù)據(jù)的過程跟CPU執(zhí)行指令的速度慢的多,因此如果任何時候對數(shù)據(jù)的操作都需要同內存進行交互,會大大降低指令執(zhí)行的速,所以在CPU中有了高速緩存。這樣,當程序在運行過程中,會將運算需要的數(shù)據(jù)從主存復制一份到 高速緩存中,這樣CPU進行計算時,就可以直接從高速緩存中讀取和寫入數(shù)據(jù),當運算結束后,再將高速緩存的數(shù)據(jù)刷新到主存。
多核CPU中,每條線程可能運行在不同的CPU中,因此每個線程都有自己的高速緩存,因此,對于一個變量,在多線程運行中,可能在多個CPU中都有改變量的高速緩存,這樣對該該變量就有可能出現(xiàn)緩存不一致的問題.
并發(fā)編程中的三個問題: 原子性問題、可見性問題、有序性問題。
原子性:即一個操作或者多個操作,要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就不執(zhí)行。

在java中,對基本數(shù)據(jù)類型的變量的讀取和賦值操作是原子性,要么執(zhí)行,要么不執(zhí)行。只有簡單的讀取、賦值才是原子操作(變量之間的操作就不是原子操作)。如果實現(xiàn)更大范圍的原子性,可以通過synchronizeLock實現(xiàn),能夠保證任一時刻只能由一個線程執(zhí)行該代碼塊,這樣就不存在原子性問題了。

可見性:當多個線程訪問一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看的到修改的值。

Java 提供了volatile關鍵字來保證可見性,當一個變量被其修飾時,它會保存修改立即更新到主存,當其他線程需要獲取時,它會去主存中讀取新值。 synchronizedLock能保證同一時刻只有一個線程獲取鎖然后執(zhí)行同步代碼,并且在釋放鎖后將變量的修改刷新到主存,因此可以保證可見性。

有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。

Java可以通過volatile關鍵字來保證一定的有序性,synchronizedLock也可保證有序性

指令重排:一般來說,處理器為了提高程序的執(zhí)行效率,可能對輸入的代碼進行優(yōu)化,它不保證程序中的各個語句的執(zhí)行順序和代碼中的順序一致,但它會保證程序最終的執(zhí)行結果和代碼順序執(zhí)行的記過是一致的。
要想并發(fā)程序正確的執(zhí)行,必須保證原子性、可見性、有序性,只要一個沒有被保證,就有可能導致程序運行不正確。

Java內存模型具備一些先天的有序性,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為 happens-before 原則。如果兩個操作的執(zhí)行次序無法從happens-before原則推導出來,那么它們就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序。
happens-before原則:

  • 程序次序規(guī)則:一個線程內,按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作
  • 鎖定規(guī)則:一個unLock操作先行發(fā)生于后面對同一個鎖的lock操作
  • volatile變量規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作
  • 傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C
  • 線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每一個動作
  • 線程中斷規(guī)則:對線程interrupt()方法的調用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生
  • 線程終結規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執(zhí)行
  • 對象終結規(guī)則:一個對象的初始化完成先行發(fā)生于他的finalize()方法的開始

volatile:一旦一個共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義: 1) 保證了不同線程對這個變量進行操作的可見性,即一個線程改變了某個變量的值,則新值對其他線程來說時立即可見的。 2) 禁止指令重排序。
最終結果:volatile可以保證操作的可見性、有序性,但不能保證操作變量的原子性。
volatile關鍵字參考Java并發(fā)編程:volatile關鍵字解析 - Matrix海子 - 博客園 (cnblogs.com)

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

友情鏈接更多精彩內容