溫馨提示×

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

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

怎么使用單例模式

發(fā)布時(shí)間:2021-10-15 16:17:09 來(lái)源:億速云 閱讀:148 作者:iii 欄目:編程語(yǔ)言

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

靜態(tài)變量實(shí)現(xiàn)單例——餓漢

保證一個(gè)實(shí)例很簡(jiǎn)單,只要每次返回同一個(gè)實(shí)例就可以,關(guān)鍵是如何保證實(shí)例化過(guò)程的線程安全?

這里先回顧下類的初始化

在類實(shí)例化之前,JVM會(huì)執(zhí)行類加載。

而類加載的最后一步就是進(jìn)行類的初始化,在這個(gè)階段,會(huì)執(zhí)行類構(gòu)造器<clinit>方法,其主要工作就是初始化類中靜態(tài)的變量,代碼塊。

<clinit>()方法是阻塞的,在多線程環(huán)境下,如果多個(gè)線程同時(shí)去初始化一個(gè)類,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類的<clinit>(),其他線程都會(huì)被阻塞。換句話說(shuō),<clinit>方法被賦予了線程安全的能力。

再結(jié)合我們要實(shí)現(xiàn)的單例,就很容易想到可以通過(guò)靜態(tài)變量的形式創(chuàng)建這個(gè)單例,這個(gè)過(guò)程是線程安全的,所以我們得出了第一種單例實(shí)現(xiàn)方法:

private static Singleton singleton = new Singleton();

public static Singleton getSingleton() {
      return singleton;
}
 

很簡(jiǎn)單,就是通過(guò)靜態(tài)變量實(shí)現(xiàn)唯一單例,并且是線程安全的。

看似比較完美的一個(gè)方法,也是有缺點(diǎn)的,就是有可能我還沒(méi)有調(diào)用getSingleton方法的時(shí)候,就進(jìn)行了類的加載,比如用到了反射或者類中其他的靜態(tài)變量靜態(tài)方法。所以這個(gè)方法的缺點(diǎn)就是有可能會(huì)造成資源浪費(fèi),在我沒(méi)用到這個(gè)單例的時(shí)候就對(duì)單例進(jìn)行了實(shí)例化。

在同一個(gè)類加載器下,一個(gè)類型只會(huì)被初始化一次,一共有六種能夠觸發(fā)類初始化的時(shí)機(jī):

  • 1、虛擬機(jī)啟動(dòng)時(shí),初始化包含 main 方法的主類;
  • 2、new等指令創(chuàng)建對(duì)象實(shí)例時(shí)
  • 3、訪問(wèn)靜態(tài)方法或者靜態(tài)字段的指令時(shí)
  • 4、子類的初始化過(guò)程如果發(fā)現(xiàn)其父類還沒(méi)有進(jìn)行過(guò)初始化
  • 5、使用反射API 進(jìn)行反射調(diào)用時(shí)
  • 6、第一次調(diào)用java.lang.invoke.MethodHandle實(shí)例時(shí)

這種我不管你用不用,只要我這個(gè)類初始化了,我就要實(shí)例化這個(gè)單例,被類比為 餓漢方法。(是真餓了,先實(shí)例化出來(lái)放著吧,要吃的時(shí)候就可以直接吃了)

缺點(diǎn)就是 有可能造成資源浪費(fèi)(到最后,飯也沒(méi)吃上,飯就浪費(fèi)了)

但其實(shí)這種模式一般也夠用了,因?yàn)橐话闱闆r下用到這個(gè)實(shí)例的時(shí)候才會(huì)去用這個(gè)類,很少存在需要使用這個(gè)類但是不使用其單例的時(shí)候。

當(dāng)然,話不能說(shuō)絕了,也是有更好的辦法來(lái)解決這種可能的資源浪費(fèi)。

在這之前,我們先看看Kotlin的 餓漢實(shí)現(xiàn)。

 

kotlin 餓漢 —— 最簡(jiǎn)單單例

object Singleton
 

沒(méi)了?嗯,沒(méi)了。

這里涉及到一個(gè)kotlin中才有的關(guān)鍵字:object(對(duì)象)

關(guān)于object主要有三種用法:

  • 對(duì)象表達(dá)式

主要用于創(chuàng)建一個(gè)繼承自某個(gè)(或某些)類型的匿名類的對(duì)象。

window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { /*……*/ }

    override fun mouseEntered(e: MouseEvent) { /*……*/ }
})
 
  • 對(duì)象聲明

主要用于單例。也就是我們今天用到的用法。

object Singleton
 

我們可以通過(guò)Android Studio 的 Show Kotlin Bytecode 功能,看到反編譯后的java代碼:

public final class Singleton {
   public static final Singleton INSTANCE;

   private Singleton() {
   }

   static {
      Singleton var0 = new Singleton();
      INSTANCE = var0;
   }
}
 

很顯然,跟我們上一節(jié)寫(xiě)的餓漢差不多,都是在類的初始化階段就會(huì)實(shí)例化出來(lái)單例,只不過(guò)一個(gè)是通過(guò)靜態(tài)代碼塊,一個(gè)是通過(guò)靜態(tài)變量。

  • 伴生對(duì)象

類內(nèi)部的對(duì)象聲明可以用 companion 關(guān)鍵字標(biāo)記,有點(diǎn)像靜態(tài)變量,但是并不是真的靜態(tài)變量。

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}

//使用
MyClass.create()
 

反編譯成Java代碼:

public final class MyClass {
   public static final MyClass.Factory Factory = new MyClass.Factory((DefaultConstructorMarker)null);
   public static final class Factory {
      @NotNull
      public final MyClass create() {
         return new MyClass();
      }

      private Factory() {
      }

      // $FF: synthetic method
      public Factory(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}
 

其原理還是一個(gè)靜態(tài)內(nèi)部類,最終調(diào)用的還是這個(gè)靜態(tài)內(nèi)部類的方法,只不過(guò)省略了靜態(tài)內(nèi)部類的名稱。

要想實(shí)現(xiàn)真正的靜態(tài)成員需要 @JvmField 修飾變量。

 

優(yōu)化餓漢,吃飯的時(shí)候再去做飯 —— 最優(yōu)雅單例

說(shuō)回正題,即然餓漢有缺點(diǎn),我們就想辦法去解決,有什么辦法可以不浪費(fèi)這個(gè)實(shí)例呢?也就是達(dá)到 按需加載 單例?

這就要涉及到另外一個(gè)知識(shí)點(diǎn)了,靜態(tài)內(nèi)部類的加載時(shí)機(jī)。

剛才說(shuō)到類的加載時(shí)候,初始化過(guò)程只會(huì)加載靜態(tài)變量和代碼塊,所以是不會(huì)加載靜態(tài)內(nèi)部類的。

靜態(tài)內(nèi)部類是延時(shí)加載的,意思就是說(shuō)只有在明確用到內(nèi)部類時(shí)才加載。只使用外部類時(shí)不加載。

根據(jù)這個(gè)信息,我們就可以優(yōu)化剛才的 餓漢模式,改成靜態(tài)內(nèi)部類模式(java和kotlin版本)

    private static class SingletonHolder {
        private static Singleton INSTANCE = new Singleton();
    }

    public static Singleton getSingleton() {
        return SingletonHolder.INSTANCE;
    }
 
 companion object {
        val instance = SingletonHolder.holder
    }

    private object SingletonHolder {
        val holder = SingletonDemo()
    }
 

同樣是通過(guò)類的初始化<clinit>()方法保證線程安全,并且在此之上,將單例的實(shí)例化過(guò)程向后移,移到靜態(tài)內(nèi)部類。所以就變成了當(dāng)調(diào)用getSingleton方法的時(shí)候才會(huì)去初始化這個(gè)靜態(tài)內(nèi)部類,也就是才會(huì)實(shí)例化靜態(tài)單例。

如此一整,這種方法就完美了...嗎?好像也有缺點(diǎn)啊,比如我調(diào)用getSingleton方法創(chuàng)建實(shí)例的時(shí)候想傳入?yún)?shù)怎么辦呢?

可以,但是需要一開(kāi)始就設(shè)置好參數(shù)值,無(wú)法通過(guò)調(diào)用getSingleton方法來(lái)動(dòng)態(tài)設(shè)置參數(shù)。比如這樣寫(xiě):

    private static class SingletonHolder {
        private static String test="123";
        private static Singleton INSTANCE = new Singleton(test);
    }

    public static Singleton getSingleton() {
        SingletonHolder.test="12345";
        return SingletonHolder.INSTANCE;
    }
 

最終實(shí)例化進(jìn)去的test只會(huì)是123,而不是12345。因?yàn)橹灰汩_(kāi)始用到SingletonHolder內(nèi)部類,單例INSTANCE就會(huì)最開(kāi)始完成了實(shí)例化,即使你賦值了test,也是單例實(shí)例化之后的事了。

這個(gè)就是 靜態(tài)內(nèi)部類方法的缺點(diǎn)了。如果不用動(dòng)態(tài)傳參數(shù),那么這個(gè)方法已經(jīng)足夠了。

 

可以傳參的單例 —— 懶漢

如果需要傳參數(shù)呢?

那就正常寫(xiě)唄,也就是調(diào)用getSingleton方法的時(shí)候,去判斷這個(gè)單例是否已存在,不存在就實(shí)例化即可。

    private static Singleton singleton;

    public static Singleton getSingleton() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
 

這個(gè)倒是看的很清楚,需要的時(shí)候才去創(chuàng)建實(shí)例,這樣的話就保證了在需要吃飯的時(shí)候才去做飯,比較中規(guī)中矩的一個(gè)做法,但是在餓漢的思維里就會(huì)覺(jué)得這個(gè)人好懶啊,都不先準(zhǔn)備好飯。

所以這個(gè)方法被稱為 懶漢式。

但是這個(gè)方法的弊端也是很明顯,就是線程不安全,不同線程同時(shí)訪問(wèn)getSingleton方法有可能導(dǎo)致對(duì)象實(shí)例化出錯(cuò)。

所以,加鎖。

 

雙重校驗(yàn)的懶漢

加鎖怎么加,也是個(gè)問(wèn)題。

首先肯定的是,我們加的鎖肯定是類鎖,因?yàn)橐槍?duì)這個(gè)類進(jìn)行加鎖,保證同一時(shí)間只有一個(gè)線程進(jìn)行單例的實(shí)例化操作。

那么類鎖就有兩種加法了,修飾靜態(tài)方法和修飾類對(duì)象:

//方法1,修飾靜態(tài)方法
    public synchronized static Singleton getSingleton() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }

//方法2,代碼塊修飾類對(duì)象
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }

        }
        return singleton;
    }
 

方法2這種方式就是我們常說(shuō)的雙重校驗(yàn)的模式。

比較下兩種方式其實(shí)區(qū)別也就是在這個(gè)雙重校驗(yàn),首先判斷單例是否為空,如果為空再進(jìn)入加鎖階段,正常走單例的實(shí)例化代碼。

那么,為什么要這么做呢?

  • 第一個(gè)判斷,是為了性能。當(dāng)這個(gè)singleton已經(jīng)實(shí)例化之后,我們?cè)偃≈灯鋵?shí)是不需要再進(jìn)入加鎖階段的,所以第一個(gè)判斷就是為了減少加鎖。把加鎖只控制在第一次實(shí)例化這個(gè)過(guò)程中,后續(xù)就可以直接獲取單例即可。
  • 第二個(gè)判斷,是防止重復(fù)創(chuàng)建對(duì)象。當(dāng)兩個(gè)線程同時(shí)走到     synchronized這里,線程A獲得鎖,進(jìn)入創(chuàng)建對(duì)象。創(chuàng)建完對(duì)象后釋放鎖,然后線程B獲得鎖,如果這時(shí)候沒(méi)有判斷單例是否為空,那么就會(huì)再次創(chuàng)建對(duì)象,重復(fù)了這個(gè)操作。

到這里,看似問(wèn)題都解決了。

等等,new Singleton()這個(gè)實(shí)例化過(guò)程真的沒(méi)問(wèn)題嗎?

在JVM中,有一種操作叫做指令重排

JVM為了優(yōu)化指令,提高程序運(yùn)行效率,在不影響單線程程序執(zhí)行結(jié)果的前提下,會(huì)將指令進(jìn)行重新排序,但是這種重新排序不會(huì)對(duì)單線程程序產(chǎn)生影響。

簡(jiǎn)單的說(shuō),就是在不影響最終結(jié)果的情況下,一些指令順序可能會(huì)被打亂。

再看看在對(duì)象實(shí)例化中的指令主要有這三步操作:

  • 1、分配對(duì)象內(nèi)存空間
  • 2、初始化對(duì)象
  • 3、instance指向剛分配的內(nèi)存地址

如果我們將第二步和第三步重排一下,結(jié)果也是不影響的:

  • 1、分配對(duì)象內(nèi)存空間
  • 2、instance指向剛分配的內(nèi)存地址
  • 3、初始化對(duì)象

這種情況下,就有問(wèn)題了:

當(dāng)線程A進(jìn)入實(shí)例化階段,也就是new Singleton(),剛完成第二步分配好內(nèi)存地址。這時(shí)候線程B調(diào)用了getSingleton()方法,走到第一個(gè)判空,發(fā)現(xiàn)不為空,返回單例,結(jié)果用的時(shí)候就有問(wèn)題了,對(duì)象都沒(méi)有初始化完成。

這就是指令重排有可能導(dǎo)致的問(wèn)題。

所以,我們需要禁止指令重排,volatile 登場(chǎng)。

volatile 主要有兩個(gè)特性:

  • 可見(jiàn)性。也就是寫(xiě)操作會(huì)對(duì)其他線程可見(jiàn)。
  • 禁止指令重排。

所以再加上volatile 對(duì)變量進(jìn)行修飾,這個(gè)雙重校驗(yàn)的單例模式也就完整了。

private volatile static Singleton singleton;     

kotlin 版本雙重校驗(yàn)

//不帶參數(shù)
class Singleton private constructor() {
    companion object {
        val instance: Singleton by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
        Singleton() }
    }
}

//帶參數(shù)
class Singleton private constructor(private val context: Context) {
    companion object {
        @Volatile private var instance: Singleton? = null

        fun getInstance(context: Context) =
                instance ?: synchronized(this) {
                    instance ?: Singleton(context).apply { 
                     instance = this 
                    }
                }
    }
}
 

誒?不帶參數(shù)的這個(gè)寫(xiě)法也太簡(jiǎn)便了點(diǎn)吧?Volatile也沒(méi)有了?確定沒(méi)問(wèn)題?

沒(méi)問(wèn)題,奧秘就在這個(gè)延遲屬性lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED)中,我們進(jìn)去瞧瞧:

public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }
 

看到了吧,其實(shí)內(nèi)部還是用到了Volatile + synchronized 雙重校驗(yàn)。 

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

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

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

AI