真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

怎么使用單例模式

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

創(chuàng)新互聯(lián)是一家集網(wǎng)站建設(shè),平橋企業(yè)網(wǎng)站建設(shè),平橋品牌網(wǎng)站建設(shè),網(wǎng)站定制,平橋網(wǎng)站建設(shè)報價,網(wǎng)絡(luò)營銷,網(wǎng)絡(luò)優(yōu)化,平橋網(wǎng)站推廣為一體的創(chuàng)新建站企業(yè),幫助傳統(tǒng)企業(yè)提升企業(yè)形象加強(qiáng)企業(yè)競爭力。可充分滿足這一群體相比中小企業(yè)更為豐富、高端、多元的互聯(lián)網(wǎng)需求。同時我們時刻保持專業(yè)、時尚、前沿,時刻以成就客戶成長自我,堅持不斷學(xué)習(xí)、思考、沉淀、凈化自己,讓我們?yōu)楦嗟钠髽I(yè)打造出實(shí)用型網(wǎng)站。

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

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

這里先回顧下類的初始化。

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

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

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

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

private static Singleton singleton = new Singleton();

public static Singleton getSingleton() {
      return singleton;
}
 

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

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

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

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

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

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

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

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

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

 

kotlin 餓漢 —— 最簡單單例

object Singleton
 

沒了?嗯,沒了。

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

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

  • 對象表達(dá)式

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

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

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

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

object Singleton
 

我們可以通過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é)寫的餓漢差不多,都是在類的初始化階段就會實(shí)例化出來單例,只不過一個是通過靜態(tài)代碼塊,一個是通過靜態(tài)變量。

  • 伴生對象

類內(nèi)部的對象聲明可以用 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();
      }
   }
}
 

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

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

 

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

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

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

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

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

根據(jù)這個信息,我們就可以優(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()
    }
 

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

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

可以,但是需要一開始就設(shè)置好參數(shù)值,無法通過調(diào)用getSingleton方法來動態(tài)設(shè)置參數(shù)。比如這樣寫:

    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只會是123,而不是12345。因?yàn)橹灰汩_始用到SingletonHolder內(nèi)部類,單例INSTANCE就會最開始完成了實(shí)例化,即使你賦值了test,也是單例實(shí)例化之后的事了。

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

 

可以傳參的單例 —— 懶漢

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

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

    private static Singleton singleton;

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

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

所以這個方法被稱為 懶漢式

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

所以,加鎖。

 

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

加鎖怎么加,也是個問題。

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

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

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

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

        }
        return singleton;
    }
 

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

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

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

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

到這里,看似問題都解決了。

等等,new Singleton()這個實(shí)例化過程真的沒問題嗎?

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

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

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

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

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

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

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

這種情況下,就有問題了:

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

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

所以,我們需要禁止指令重排,volatile 登場。

volatile 主要有兩個特性:

  • 可見性。也就是寫操作會對其他線程可見。
  • 禁止指令重排。

所以再加上volatile 對變量進(jìn)行修飾,這個雙重校驗(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ù)的這個寫法也太簡便了點(diǎn)吧?Volatile也沒有了?確定沒問題?

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

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

private class SynchronizedLazyImpl(initializer: () -> T, lock: Any? = null) : Lazy, 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)的知識可以關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!


網(wǎng)頁標(biāo)題:怎么使用單例模式
文章地址:http://weahome.cn/article/gepghd.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部