Kotlin 如何實現(xiàn)常用單例模式
從這篇文章開始,我將帶領大家一起來進入 Kotlin 實戰(zhàn)篇,俗話說光說不練假把式,下面該系列就是闡述 Kotlin 是如何用于平時的開發(fā),這篇文章將帶領大家如何使用 Kotlin 實現(xiàn)常見的設計模式,相比 Java 實現(xiàn)設計模式,Kotlin 就會顯得簡潔和高效沒有冗余的模板代碼。這篇文章介紹的是最為簡單和常用的單利模式。
1. 單例模式的介紹
單例模式是開發(fā)者最為常見的一種設計模式,也是 23 種設計模式中最為簡單一種設計模式。大部分的開發(fā)者都知道它的使用和原理。單例模式顧名思義就是在應用這個模式時,單例對象的類必須是只有一個對象實例存在。在一些應用場景中我們只需要一個全局唯一的對象實例去調(diào)度整體行為。還有一些情況為了系統(tǒng)資源開銷考慮,避免重復創(chuàng)建多個實例,往往采用單例模式來保證全局只有一個實例對象。
2. 單例模式的定義
保證某個類只有一個實例對象,該實例對象在內(nèi)部進行實例化,并且提供了一個獲取該實例對象的全局訪問點。
3. 單例模式的基本要求
- 1、構造器私有化,private 修飾,主要為了防止外部私自創(chuàng)建該單例類的對象實例
- 2、提供一個該實例對象全局訪問點,在 Java 中一般是以公有的靜態(tài)方法或者枚舉返回單例類對象
- 3、在多線程環(huán)境下保證單例類有且只有一個對象實例,以及在多線程環(huán)境下獲取單例類對象實例需要保證線程安全。
- 4、在反序列化時保證單例類有且只有一個對象實例。
4. 單例模式的使用場景
一般用于確定某個類只需要一個實例對象,從而避免中了頻繁創(chuàng)建多個對象實例所帶來資源和性能開銷。例如常見的數(shù)據(jù)庫連接或 IO 操作等。
5. 單例模式的 UML 類圖
6. 餓漢式單例
餓漢式單例模式是實現(xiàn)單例模式比較簡單的一種方式,它有個特點就是不管需不需要該單例實例,該實例對象都會被實例化。
6.1 Kotlin 實現(xiàn)餓漢式單例
在 Kotlin 中實現(xiàn)一個餓漢式單例模式可以說是非常非常簡單,只需要定義一個 object 對象表達式即可,無需手動去設置構造器私有化和提供全局訪問點,這一點 Kotlin 編譯器全給你做好了。
object KSingleton : Serializable {//實現(xiàn)Serializable序列化接口,通過私有、被實例化的readResolve方法控制反序列化
fun doSomething() {
println("do some thing")
}
private fun readResolve(): Any {//防止單例對象在反序列化時重新生成對象
return KSingleton//由于反序列化時會調(diào)用readResolve這個鉤子方法,只需要把當前的KSingleton對象返回而不是去創(chuàng)建一個新的對象
}
}
//在Kotlin中使用KSingleton
fun main(args: Array<String>) {
KSingleton.doSomething()//像調(diào)用靜態(tài)方法一樣,調(diào)用單例類中的方法
}
//在Java中使用KSingleton
public class TestMain {
public static void main(String[] args) {
KSingleton.INSTANCE.doSomething();//通過拿到KSingleton的公有單例類靜態(tài)實例INSTANCE, 再通過INSTANCE調(diào)用單例類中的方法
}
}
KSingleton 反編譯成 Java 代碼
public final class KSingleton implements Serializable {
public static final KSingleton INSTANCE;
public final void doSomething() {
String var1 = "do some thing";
System.out.println(var1);
}
private final Object readResolve() {
return INSTANCE;//可以看到readResolve方法直接返回了INSTANCE而不是創(chuàng)建新的實例
}
static {//靜態(tài)代碼塊初始化KSingleton實例,不管有沒有使用,只要KSingleton被加載了,
//靜態(tài)代碼塊就會被調(diào)用,KSingleton實例就會被創(chuàng)建,并賦值給INSTANCE
KSingleton var0 = new KSingleton();
INSTANCE = var0;
}
}
可能會有人疑問:沒有看到構造器私有化,實際上這一點已經(jīng)在編譯器層面做了限制,不管你是在 Java 還是 Kotlin 中都無法私自去創(chuàng)建新的單例對象。
6.2 Java 實現(xiàn)餓漢式單例
public class Singleton implements Serializable {
private Singleton() {//構造器私有化
}
private static final Singleton mInstance = new Singleton();
public static Singleton getInstance() {//提供公有獲取單例對象的函數(shù)
return mInstance;
}
//防止單例對象在反序列化時重新生成對象
private Object readResolve() throws ObjectStreamException {
return mInstance;
}
}
對比一下 Kotlin 和 Java 的餓漢式的單例實現(xiàn)發(fā)現(xiàn),是不是覺得 Kotlin 會比 Java 簡單得多得多。
7. 線程安全的懶漢式單例
可是有時候我們并不想當類加載的時候就去創(chuàng)建這個單例實例,而是想當我們使用這個實例的時候才去初始化它。于是乎就有了懶漢式的單例模式
7.1 Kotlin 實現(xiàn)線程安全的懶漢式單例
class KLazilySingleton private constructor() : Serializable {
fun doSomething() {
println("do some thing")
}
companion object {
private var mInstance: KLazilySingleton? = null
get() {
return field ?: KLazilySingleton()
}
@JvmStatic
@Synchronized//添加synchronized同步鎖
fun getInstance(): KLazilySingleton {
return requireNotNull(mInstance)
}
}
//防止單例對象在反序列化時重新生成對象
private fun readResolve(): Any {
return KLazilySingleton.getInstance()
}
}
//在Kotlin中調(diào)用
fun main(args: Array<String>) {
KLazilySingleton.getInstance().doSomething()
}
//在Java中調(diào)用
KLazilySingleton.getInstance().doSomething();
7.2 Java 實現(xiàn)線程安全的懶漢式單例
class LazilySingleton implements Serializable {
private static LazilySingleton mInstance;
private LazilySingleton() {}//構造器私有化
public static synchronized LazilySingleton getInstance() {//synchronized同步鎖保證多線程調(diào)用getInstance方法線程安全
if (mInstance == null){
mInstance = new LazilySingleton();
}
return mInstance;
}
private Object readResolve() throws ObjectStreamException {//防止反序列化
return mInstance;
}
}
8. DCL (double check lock) 改造懶漢式單例
我們知道線程安全的單例模式直接是使用 synchronized 同步鎖,鎖住 getInstance 方法,每一次調(diào)用該方法的時候都得獲取鎖,但是如果這個單例已經(jīng)被初始化了,其實按道理就不需要申請同步鎖了,直接返回這個單例類實例即可。于是就有了 DCL 實現(xiàn)單例方式。
8.1 Java 中 DCL 實現(xiàn)
//DCL實現(xiàn)單例模式
public class LazySingleTon implements Serializable {
//靜態(tài)成員私有化,注意使用volatile關鍵字,因為會存在DCL失效的問題
private volatile static LazySingleTon mInstance = null;
private LazySingleTon() { //構造器私有化
}
//公有獲取單例對象的函數(shù)
//DCL(Double Check Lock) 既能在需要的時候初始化單例,又能保證線程安全,且單例對象初始化完后,調(diào)用getInstance不需要進行同步鎖
public static LazySingleTon getInstance() {
if (mInstance == null) {//為了防止單例對象初始化完后,調(diào)用getInstance再次重復進行同步鎖
synchronized (LazySingleTon.class) {
if (mInstance == null) {
mInstance = new LazySingleTon();
}
}
}
return mInstance;
}
private Object readResolve() throws ObjectStreamException {
return mInstance;
}
}
8.2 Kotlin 中 DCL 實現(xiàn)
在 Kotlin 中有個天然特性可以支持線程安全 DCL 的單例,可以說也是非常非常簡單,就僅僅 3 行代碼左右,那就是 Companion Object + lazy 屬性代理,一起來看下吧。
class KLazilyDCLSingleton private constructor() : Serializable {//private constructor()構造器私有化
fun doSomething() {
println("do some thing")
}
private fun readResolve(): Any {//防止單例對象在反序列化時重新生成對象
return instance
}
companion object {
//通過@JvmStatic注解,使得在Java中調(diào)用instance直接是像調(diào)用靜態(tài)函數(shù)一樣,
//類似KLazilyDCLSingleton.getInstance(),如果不加注解,在Java中必須這樣調(diào)用: KLazilyDCLSingleton.Companion.getInstance().
@JvmStatic
//使用lazy屬性代理,并指定LazyThreadSafetyMode為SYNCHRONIZED模式保證線程安全
val instance: KLazilyDCLSingleton by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { KLazilyDCLSingleton() }
}
}
//在Kotlin中調(diào)用,直接通過KLazilyDCLSingleton類名調(diào)用instance
fun main(args: Array<String>) {
KLazilyDCLSingleton.instance.doSomething()
}
//在Java中調(diào)用
public class TestMain {
public static void main(String[] args) {
//加了@JvmStatic注解后,可以直接KLazilyDCLSingleton.getInstance(),不會打破Java中調(diào)用習慣,和Java調(diào)用方式一樣。
KLazilyDCLSingleton.getInstance().doSomething();
//沒有加@JvmStatic注解,只能這樣通過Companion調(diào)用
KLazilyDCLSingleton.Companion.getInstance().doSomething();
}
}
注意:建議上面例子中添加 @JvmStatic 注解,Kotlin 這門語言可謂是操碎了心,做的很小心翼翼,為了不讓 Java 開發(fā)者打破他們的調(diào)用習慣,讓調(diào)用根本無法感知到是 Kotlin 編寫,因為外部調(diào)用方式和 Java 方式一樣。如果硬生生把 Companion 對象暴露給 Java 開發(fā)者他們可能會感到一臉懵逼。
可能大家對 lazy 和 Companion Object 功能強大感到一臉懵,讓我們一起瞅瞅反編譯后的 Java 代碼你就會恍然大悟了:
public final class KLazilyDCLSingleton implements Serializable {
@NotNull
private static final Lazy instance$delegate;
//Companion提供公有全局訪問點,KLazilyDCLSingleton.Companion實際上一個餓漢式的單例模式
public static final KLazilyDCLSingleton.Companion Companion = new KLazilyDCLSingleton.Companion((DefaultConstructorMarker)null);
public final void doSomething() {
String var1 = "do some thing";
System.out.println(var1);
}
private final Object readResolve() {
return Companion.getInstance();
}
private KLazilyDCLSingleton() {
}
static {//注意: 可以看到靜態(tài)代碼塊中并不是初始化KLazilyDCLSingleton的instance而是初始化它的Lazy代理對象,說明KLazilyDCLSingleton類被加載了,
//但是KLazilyDCLSingleton的instance并沒有被初始化,符合懶加載規(guī)則,那么什么時候初始化instance這就涉及到了屬性代理知識了,下面會做詳細分析
instance$delegate = LazyKt.lazy(LazyThreadSafetyMode.SYNCHRONIZED, (Function0)null.INSTANCE);
}
// $FF: synthetic method
public KLazilyDCLSingleton(DefaultConstructorMarker $constructor_marker) {
this();
}
@NotNull
public static final KLazilyDCLSingleton getInstance() {
return Companion.getInstance();//這里可以看到加了@JvmStatic注解后,getInstance內(nèi)部把我們省略Companion.getInstance()這一步,這樣一來Java調(diào)用者就直接KLazilyDCLSingleton.getInstance()獲取單例實例
}
//Companion靜態(tài)內(nèi)部類實際上也是一個單例模式
public static final class Companion {
// $FF: synthetic field
static final KProperty[] ?delegatedProperties = new KProperty[]{(KProperty)Reflection.property1(new PropertyReference1Impl(Reflection.getOrCreateKotlinClass(KLazilyDCLSingleton.Companion.class), "instance", "getInstance()Lcom/mikyou/design_pattern/singleton/kts/KLazilyDCLSingleton;"))};
/** @deprecated */
// $FF: synthetic method
@JvmStatic
public static void instance$annotations() {
}
@NotNull
//這個方法需要注意,最終instance初始化和獲取將在這里進行
public final KLazilyDCLSingleton getInstance() {
//拿到代理對象
Lazy var1 = KLazilyDCLSingleton.instance$delegate;
KProperty var3 = ?delegatedProperties[0];
//代理對象的getValue方法就是初始化instance和獲取instance的入口。內(nèi)部會判斷instance是否被初始化過沒有就會返回新創(chuàng)建的對象,
//初始化過直接返回上一次初始化的對象。所以只有真正調(diào)用getInstance方法需要這個實例的時候instance才會被初始化。
return (KLazilyDCLSingleton)var1.getValue();
}
private Companion() {//Companion構造器私有化
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
8.3 Kotlin 的 lazy 屬性代理內(nèi)部實現(xiàn)源碼分析
//expect關鍵字標記這個函數(shù)是平臺相關,我們需要找到對應的actual關鍵字實現(xiàn)表示平臺中一個相關實現(xiàn)
public expect fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T>
//對應多平臺中一個平臺相關實現(xiàn)lazy函數(shù)
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {//根據(jù)不同mode,返回不同的Lazy的實現(xiàn),我們重點看下SynchronizedLazyImpl
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//為了解決DCL帶來指令重排序?qū)е轮鞔婧凸ぷ鲀?nèi)存數(shù)據(jù)不一致的問題,這里使用Volatile原語注解。具體Volatile為什么能解決這樣的問題請接著看后面的分析
private val lock = lock ?: this
override val value: T
get() {//當外部調(diào)用value值,get訪問器會被調(diào)用
val _v1 = _value
if (_v1 !== UNINITIALIZED_VALUE) {//進行第一層的Check, 如果這個值已經(jīng)初始化過了,直接返回_v1,避免走下面synchronized獲取同步鎖帶來不必要資源開銷。
@Suppress("UNCHECKED_CAST")
return _v1 as T
}
return synchronized(lock) {
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {//進行第二層的Check,主要是為了_v2被初始化直接返回
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
//如果沒有初始化執(zhí)行initializer!!() lambda,
//實際上相當于執(zhí)行外部調(diào)用傳入的 by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { KLazilyDCLSingleton() } 中的KLazilyDCLSingleton()也即是返回KLazilyDCLSingleton實例對象
val typedValue initializer!!()
_value = typedValue//并把這個實例對象保存在_value中
initializer = null
typedValue
}
}
}
override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE
override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."
private fun writeReplace(): Any = InitializedLazyImpl(value)
}
8.4 DCL 存在多線程安全問題分析及解決
DCL 存在多線程安全問題,我們都知道線程安全主要來自主存和工作內(nèi)存數(shù)據(jù)不一致以及重排序 (指令重排序或編譯器重排序造成的)。那么 DCL 存在什么問題呢?
首先,mInstance = new LazySingleton()
不是一個原子操作而是分為三步進行:
- 1、給 LazySingleton 實例分配內(nèi)存;
- 2、調(diào)用 LazySingleton 的構造函數(shù),初始化成員字段;
- 3、將 mInstance 對象引用指向分配的內(nèi)存空間 (此時 mInstance 不為 null)。
在 JDK1.5 之前版本的 Java 內(nèi)存模型中,Cache, 寄存器到主存回寫順序規(guī)則,無法保證第 2 和第 3 執(zhí)行的順序,可能是 1-2-3,也有可能是 1-3-2。
若 A 線程先執(zhí)行了第 1 步,第 3 步,此時切換到 B 線程,由于 A 線程中已經(jīng)執(zhí)行了第 3 步所以 mInstance 不為 null,那么 B 線程中直接把 mInstance 取走,由于并沒有執(zhí)行第 2 步使用的時候就會報錯。
為了解決該問題,JDK1.5 之后,具體化了 volatile
關鍵字,能夠確保每次都是從主存獲取最新有效值。所以需要 private volatile static LazySingleTon mInstance = null;
9. 靜態(tài)內(nèi)部類單例
DCL 雖然在一定程度上能解決資源消耗、多余 synchronized 同步、線程安全等問題,但是某些情況下還會存在 DCL 失效問題,盡管在 JDK1.5 之后通過具體化 volatile 原語來解決 DCL 失效問題,但是它始終并不是優(yōu)雅一種解決方式,在多線程環(huán)境下一般不推薦 DCL 的單例模式。所以引出靜態(tài)內(nèi)部類單例實現(xiàn)
9.1 Kotlin 實現(xiàn)靜態(tài)內(nèi)部類單例
class KOptimizeSingleton private constructor(): Serializable {//private constructor()構造器私有化
companion object {
@JvmStatic
fun getInstance(): KOptimizeSingleton {//全局訪問點
return SingletonHolder.mInstance
}
}
fun doSomething() {
println("do some thing")
}
private object SingletonHolder {//靜態(tài)內(nèi)部類
val mInstance: KOptimizeSingleton = KOptimizeSingleton()
}
private fun readResolve(): Any {//防止單例對象在反序列化時重新生成對象
return SingletonHolder.mInstance
}
}
9.2 Java 實現(xiàn)靜態(tài)內(nèi)部類單例
//使用靜態(tài)內(nèi)部單例模式
public class OptimizeSingleton implements Serializable {
//構造器私有化
private OptimizeSingleton() {
}
//靜態(tài)私有內(nèi)部類
private static class SingletonHolder {
private static final OptimizeSingleton sInstance = new OptimizeSingleton();
}
//公有獲取單例對象的函數(shù)
public static OptimizeSingleton getInstance() {
return SingletonHolder.sInstance;
}
public void doSomeThings() {
System.out.println("do some things");
}
//防止反序列化重新創(chuàng)建對象
private Object readResolve() {
return SingletonHolder.sInstance;
}
}
10. 枚舉單例
其實細心的小伙伴就會觀察到上面例子中我都會去實現(xiàn) Serializable
接口,并且會去實現(xiàn) readResolve
方法。這是為了反序列化會重新創(chuàng)建對象而使得原來的單例對象不再唯一。
通過序列化一個單例對象將它寫入到磁盤中,然后再從磁盤中讀取出來,從而可以獲得一個新的實例對象,即使構造器是私有的,反序列化會通過其他特殊途徑創(chuàng)建單例類的新實例。然而為了讓開發(fā)者能夠控制反序列化,提供一個特殊的鉤子方法那就是 readResolve
方法,這樣一來我們只需要在 readResolve
直接返回原來的實例即可,就不會創(chuàng)建新的對象。
枚舉單例實現(xiàn),就是為了防止反序列化,因為我們都知道枚舉類反序列化是不會創(chuàng)建新的對象實例的。 Java 的序列化機制對枚舉類型做了特殊處理,一般來說在序列枚舉類型時,只會存儲枚舉類的引用和枚舉常量名稱,反序列化的過程中,這些信息被用來在運行時環(huán)境中查找存在的枚舉類型對象,枚舉類型的序列化機制保證只會查找已經(jīng)存在的枚舉類型實例,而不是創(chuàng)建新的實例。
10.1 Kotlin 實現(xiàn)枚舉單例
enum class KEnumSingleton {
INSTANCE;
fun doSomeThing() {
println("do some thing")
}
}
//在Kotlin中調(diào)用
fun main(args: Array<String>) {
KEnumSingleton.INSTANCE.doSomeThing()
}
//在Java中調(diào)用
KEnumSingleton.INSTANCE.doSomeThing();
10.2 Java 實現(xiàn)枚舉單例
public enum EnumSingleton {
INSTANCE;
public void doSomeThing() {
System.out.println("do some thing");
}
}
//調(diào)用方式
EnumSingleton.INSTANCE.doSomeThing();
11. 總結
到這里有關 Kotlin 實現(xiàn)常見的單例模式就闡述完畢,這篇文章通過與 Java 對比,并用幾種常見單例模式舉例對比 Kotlin 和 Java 實現(xiàn)。下篇文章將繼續(xù)設計模式中代理模式,看看 Kotlin 為代理模式帶來什么神奇的魔法。