您好,登錄后才能下訂單哦!
本篇內(nèi)容介紹了“怎么使用單例模式”的有關(guān)知識(shí),在實(shí)際案例的操作過(guò)程中,不少人都會(huì)遇到這樣的困境,接下來(lái)就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!
保證一個(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)
。
object Singleton
沒(méi)了?嗯,沒(méi)了。
這里涉及到一個(gè)kotlin中才有的關(guān)鍵字:object(對(duì)象)
。
關(guān)于object主要有三種用法:
主要用于創(chuàng)建一個(gè)繼承自某個(gè)(或某些)類型的匿名類的對(duì)象。
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { /*……*/ }
override fun mouseEntered(e: MouseEvent) { /*……*/ }
})
主要用于單例。也就是我們今天用到的用法。
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)變量。
類內(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
修飾變量。
說(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ò)。
所以,加鎖。
加鎖怎么加,也是個(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í)例化中的指令主要有這三步操作:
如果我們將第二步和第三步重排一下,結(jié)果也是不影響的:
這種情況下,就有問(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è)特性:
所以再加上volatile
對(duì)變量進(jìn)行修飾,這個(gè)雙重校驗(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ù)的這個(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í)用文章!
免責(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)容。