本篇內(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)站。
保證一個實(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)
。
object Singleton
沒了?嗯,沒了。
這里涉及到一個kotlin中才有的關(guān)鍵字:object(對象)
。
關(guān)于object主要有三種用法:
主要用于創(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
修飾變量。
說回正題,即然餓漢有缺點(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)橐槍@個類進(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í)例化中的指令主要有這三步操作:
如果我們將第二步和第三步重排一下,結(jié)果也是不影響的:
這種情況下,就有問題了:
當(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;
//不帶參數(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í)用文章!