當Kotlin完美邂逅設計模式之單例模式(一)

極客熊貓發表於2019-04-07

簡述: 從這篇文章開始,我將帶領大家一起來探討一下Kotlin眼中的設計模式。說下為什麼想著要開始這麼一個系列文章。主要基於下面幾點原因:

  • 1、設計模式一直是開發者看懂Android原始碼的一個很大障礙。所以想要理解和運用原始碼中一些設計思想和技巧,首先看懂原始碼是第一步,而看懂原始碼,又得需要設計模式和資料結構演算法(我的每週一演算法和資料結構文章系列也開始了)作為基礎,否則看起來雲裡霧裡,只能死記硬背別人總結的結論,最終還是無法消化和理解運用。
  • 2、Kotlin中設計模式的實現和Java的實現還是有很大的差別的,利用Kotlin語言自身的特性實現設計模式比硬生生套用Java中的設計模式實現要更優雅和更高效。當然每個設計模式我會對比Java與Kotlin實現區別,以便理解更加深刻。
  • 3、據瞭解Kotlin有關設計模式實現的文章目前在國內還是比較少的,所以想系統地去寫一個有關Kotlin邂逅設計模式的系列文章。

說下最終的目標吧,最終目標是有基礎能力在分析的原始碼時候能夠站在一個全域性角度去思考,而不是一頭扎入茫茫原始碼中無法自拔迷失自我。後面也會隨即出一些有關原始碼分析的文章。所以請暫時先好好掌握這些基礎的工具。

一、介紹

單例模式是開發者最為常見的一種設計模式,也是23種設計模式中最為簡單一種設計模式。大部分的開發者都知道它的使用和原理。單例模式顧名思義就是在應用這個模式時,單例物件的類必須是隻有一個物件例項存在。在一些應用場景中我們只需要一個全域性唯一的物件例項去排程整體行為。還有一些情況為了系統資源開銷考慮,避免重複建立多個例項,往往採用單例模式來保證全域性只有一個例項物件。

二、定義

保證某個類只有一個例項物件,該例項物件在內部進行例項化,並且提供了一個獲取該例項物件的全域性訪問點。

三、基本要求

  • 1、構造器私有化,private修飾,主要為了防止外部私自建立該單例類的物件例項
  • 2、提供一個該例項物件全域性訪問點,在Java中一般是以公有的靜態方法或者列舉返回單例類物件
  • 3、在多執行緒環境下保證單例類有且只有一個物件例項,以及在多執行緒環境下獲取單例類物件例項需要保證執行緒安全。
  • 4、在反序列化時保證單例類有且只有一個物件例項

四、使用場景

一般用於確定某個類只需要一個例項物件,從而避免中了頻繁建立多個物件例項所帶來資源和效能開銷。例如常見的資料庫連線或IO操作等。

五、UML類圖

當Kotlin完美邂逅設計模式之單例模式(一)

六、餓漢式單例

餓漢式單例模式是實現單例模式比較簡單的一種方式,它有個特點就是不管需不需要該單例例項,該例項物件都會被例項化。

1、Kotlin實現

在Kotlin中實現一個餓漢式單例模式可以說是非常非常簡單,只需要定義一個object物件表示式即可,無需手動去設定構造器私有化和提供全域性訪問點,這一點Kotlin編譯器全給你做好了。

object KSingleton : Serializable {//實現Serializable序列化介面,通過私有、被例項化的readResolve方法控制反序列化
    fun doSomething() {
        println("do some thing")
    }

    private fun readResolve(): Any {//防止單例物件在反序列化時重新生成物件
        return KSingleton//由於反序列化時會呼叫readResolve這個鉤子方法,只需要把當前的KSingleton物件返回而不是去建立一個新的物件
    }
}

//在Kotlin中使用KSingleton
fun main(args: Array<String>) {
    KSingleton.doSomething()//像呼叫靜態方法一樣,呼叫單例類中的方法
}
//在Java中使用KSingleton
public class TestMain {
    public static void main(String[] args) {
        KSingleton.INSTANCE.doSomething();//通過拿到KSingleton的公有單例類靜態例項INSTANCE, 再通過INSTANCE呼叫單例類中的方法
    }
}
複製程式碼

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而不是建立新的例項
   }

   static {//靜態程式碼塊初始化KSingleton例項,不管有沒有使用,只要KSingleton被載入了,
   //靜態程式碼塊就會被呼叫,KSingleton例項就會被建立,並賦值給INSTANCE
      KSingleton var0 = new KSingleton();
      INSTANCE = var0;
   }
}
複製程式碼

可能會有人疑問: 沒有看到構造器私有化,實際上這一點已經在編譯器層面做了限制,不管你是在Java還是Kotlin中都無法私自去建立新的單例物件。

2、Java實現

public class Singleton implements Serializable {
    private Singleton() {//構造器私有化
    }

    private static final Singleton mInstance = new Singleton();

    public static Singleton getInstance() {//提供公有獲取單例物件的函式
        return mInstance;
    }

    //防止單例物件在反序列化時重新生成物件
    private Object readResolve() throws ObjectStreamException {
        return mInstance;
    }
}
複製程式碼

對比一下Kotlin和Java的餓漢式的單例實現發現,是不是覺得Kotlin會比Java簡單得多得多。

七、執行緒安全的懶漢式單例

可是有時候我們並不想當類載入的時候就去建立這個單例例項,而是想當我們使用這個例項的時候才去初始化它。於是乎就有了懶漢式的單例模式

1、Kotlin實現

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中呼叫
fun main(args: Array<String>) {
    KLazilySingleton.getInstance().doSomething()
}
//在Java中呼叫
 KLazilySingleton.getInstance().doSomething();
複製程式碼

2、Java實現

class LazilySingleton implements Serializable {
    private static LazilySingleton mInstance;

    private LazilySingleton() {}//構造器私有化

    public static synchronized LazilySingleton getInstance() {//synchronized同步鎖保證多執行緒呼叫getInstance方法執行緒安全
        if (mInstance == null){
            mInstance = new LazilySingleton();
        }
        return mInstance;
    }
    
    private Object readResolve() throws ObjectStreamException {//防止反序列化
        return mInstance;
    }
}
複製程式碼

八、DCL(double check lock)改造懶漢式單例

我們知道執行緒安全的單例模式直接是使用synchronized同步鎖,鎖住getInstance方法,每一次呼叫該方法的時候都得獲取鎖,但是如果這個單例已經被初始化了,其實按道理就不需要申請同步鎖了,直接返回這個單例類例項即可。於是就有了DCL實現單例方式。

1、Java中DCL實現

//DCL實現單例模式
public class LazySingleTon implements Serializable {
    //靜態成員私有化,注意使用volatile關鍵字,因為會存在DCL失效的問題
    private volatile static LazySingleTon mInstance = null; 

    private LazySingleTon() { //構造器私有化
    }

    //公有獲取單例物件的函式
    //DCL(Double Check Lock) 既能在需要的時候初始化單例,又能保證執行緒安全,且單例物件初始化完後,呼叫getInstance不需要進行同步鎖
    public static LazySingleTon getInstance() {
        if (mInstance == null) {//為了防止單例物件初始化完後,呼叫getInstance再次重複進行同步鎖
            synchronized (LazySingleTon.class) {
                if (mInstance == null) {
                    mInstance = new LazySingleTon();
                }
            }
        }

        return mInstance;
    }

    private Object readResolve() throws ObjectStreamException {
        return mInstance;
    }
}
複製程式碼

2、Kotlin中DCL實現

在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中呼叫instance直接是像呼叫靜態函式一樣,
        //類似KLazilyDCLSingleton.getInstance(),如果不加註解,在Java中必須這樣呼叫: KLazilyDCLSingleton.Companion.getInstance().
        @JvmStatic
        //使用lazy屬性代理,並指定LazyThreadSafetyMode為SYNCHRONIZED模式保證執行緒安全
        val instance: KLazilyDCLSingleton by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { KLazilyDCLSingleton() }
    }
}

//在Kotlin中呼叫,直接通過KLazilyDCLSingleton類名呼叫instance
fun main(args: Array<String>) {
    KLazilyDCLSingleton.instance.doSomething()
}
//在Java中呼叫
public class TestMain {
    public static void main(String[] args) {
    //加了@JvmStatic註解後,可以直接KLazilyDCLSingleton.getInstance(),不會打破Java中呼叫習慣,和Java呼叫方式一樣。
       KLazilyDCLSingleton.getInstance().doSomething();
       //沒有加@JvmStatic註解,只能這樣通過Companion呼叫
       KLazilyDCLSingleton.Companion.getInstance().doSomething();
    }
}
複製程式碼

注意: 建議上面例子中新增@JvmStatic註解,Kotlin這門語言可謂是操碎了心,做的很小心翼翼,為了不讓Java開發者打破他們的呼叫習慣,讓呼叫根本無法感知到是Kotlin編寫,因為外部呼叫方式和Java方式一樣。如果硬生生把Companion物件暴露給Java開發者他們可能會感到一臉懵逼。

可能大家對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 {//注意: 可以看到靜態程式碼塊中並不是初始化KLazilyDCLSingleton的instance而是初始化它的Lazy代理物件,說明KLazilyDCLSingleton類被載入了,
   //但是KLazilyDCLSingleton的instance並沒有被初始化,符合懶載入規則,那麼什麼時候初始化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內部把我們省略Companion.getInstance()這一步,這樣一來Java呼叫者就直接KLazilyDCLSingleton.getInstance()獲取單例例項
   }

   //Companion靜態內部類實際上也是一個單例模式
   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的入口。內部會判斷instance是否被初始化過沒有就會返回新建立的物件,
         //初始化過直接返回上一次初始化的物件。所以只有真正呼叫getInstance方法需要這個例項的時候instance才會被初始化。
         return (KLazilyDCLSingleton)var1.getValue();
      }

      private Companion() {//Companion構造器私有化
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
複製程式碼

3、Kotlin的lazy屬性代理內部實現原始碼分析

//expect關鍵字標記這個函式是平臺相關,我們需要找到對應的actual關鍵字實現表示平臺中一個相關實現 
public expect fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T>

//對應多平臺中一個平臺相關實現lazy函式
public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {//根據不同mode,返回不同的Lazy的實現,我們重點看下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帶來指令重排序導致主存和工作記憶體資料不一致的問題,這裡使用Volatile原語註解。具體Volatile為什麼能解決這樣的問題請接著看後面的分析
    private val lock = lock ?: this

    override val value: T
        get() {//當外部呼叫value值,get訪問器會被呼叫
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {//進行第一層的Check, 如果這個值已經初始化過了,直接返回_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 {
                //如果沒有初始化執行initializer!!() lambda, 
                //實際上相當於執行外部呼叫傳入的 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)
}    
複製程式碼

4、DCL存在多執行緒安全問題分析及解決

  • 問題分析:

DCL存在多執行緒安全問題,我們都知道執行緒安全主要來自主存和工作記憶體資料不一致以及重排序(指令重排序或編譯器重排序造成的)。那麼DCL存在什麼問題呢? 首先,mInstance = new LazySingleton() 不是一個原子操作而是分為三步進行:

  • 1、給LazySingleton例項分配記憶體
  • 2、呼叫LazySingleton的建構函式,初始化成員欄位
  • 3、將mInstance物件引用指向分配的記憶體空間(此時mInstance不為null)

在JDK1.5之前版本的Java記憶體模型中,Cache,暫存器到主存回寫順序規則,無法保證第2和第3執行的順序,可能是1-2-3,也有可能是1-3-2 若A執行緒先執行了第1步,第3步,此時切換到B執行緒,由於A執行緒中已經執行了第3步所以mInstance不為null,那麼B執行緒中直接把mInstance取走,由於並沒有執行第2步使用的時候就會報錯。

  • 解決問題:

為了解決該問題,JDK1.5之後,具體化了volatile關鍵字,能夠確保每次都是從主存獲取最新有效值。所以需要private volatile static LazySingleTon mInstance = null;

九、靜態內部類單例

DCL雖然在一定程度上能解決資源消耗、多餘synchronized同步、執行緒安全等問題,但是某些情況下還會存在DCL失效問題,儘管在JDK1.5之後通過具體化volatile原語來解決DCL失效問題,但是它始終並不是優雅一種解決方式,在多執行緒環境下一般不推薦DCL的單例模式。所以引出靜態內部類單例實現

1、Kotlin實現

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 {//靜態內部類
        val mInstance: KOptimizeSingleton = KOptimizeSingleton()
    }
    
    private fun readResolve(): Any {//防止單例物件在反序列化時重新生成物件
        return SingletonHolder.mInstance
    }
}
複製程式碼

2、Java實現

//使用靜態內部單例模式
public class OptimizeSingleton implements Serializable {
    //構造器私有化
    private OptimizeSingleton() {
    }

    //靜態私有內部類
    private static class SingletonHolder {
        private static final OptimizeSingleton sInstance = new OptimizeSingleton();
    }

    //公有獲取單例物件的函式
    public static OptimizeSingleton getInstance() {
        return SingletonHolder.sInstance;
    }
    
    public void doSomeThings() {
        System.out.println("do some things");
    }
    
    //防止反序列化重新建立物件
    private Object readResolve() {
        return SingletonHolder.sInstance;
    }
}
複製程式碼

十、列舉單例

其實細心的小夥伴就會觀察到上面例子中我都會去實現Serializable介面,並且會去實現readResolve方法。這是為了反序列化會重新建立物件而使得原來的單例物件不再唯一。通過序列化一個單例物件將它寫入到磁碟中,然後再從磁碟中讀取出來,從而可以獲得一個新的例項物件,即使構造器是私有的,反序列化會通過其他特殊途徑建立單例類的新例項。然而為了讓開發者能夠控制反序列化,提供一個特殊的鉤子方法那就是readResolve方法,這樣一來我們只需要在readResolve直接返回原來的例項即可,就不會建立新的物件。

列舉單例實現,就是為了防止反序列化,因為我們都知道列舉類反序列化是不會建立新的物件例項的。 Java的序列化機制對列舉型別做了特殊處理,一般來說在序列列舉型別時,只會儲存列舉類的引用和列舉常量名稱,反序列化的過程中,這些資訊被用來在執行時環境中查詢存在的列舉型別物件,列舉型別的序列化機制保證只會查詢已經存在的列舉型別例項,而不是建立新的例項。

1、Kotlin實現

enum class KEnumSingleton {
    INSTANCE;

    fun doSomeThing() {
        println("do some thing")
    }
}
//在Kotlin中呼叫
fun main(args: Array<String>) {
    KEnumSingleton.INSTANCE.doSomeThing()
}
//在Java中呼叫
 KEnumSingleton.INSTANCE.doSomeThing();
複製程式碼

2、Java實現

public enum EnumSingleton {
    INSTANCE;
    public void doSomeThing() {
        System.out.println("do some thing");
    }
}

//呼叫方式
EnumSingleton.INSTANCE.doSomeThing();
複製程式碼
當Kotlin完美邂逅設計模式之單例模式(一)

歡迎關注Kotlin開發者聯盟,這裡有最新Kotlin技術文章,每週會不定期翻譯一篇Kotlin國外技術文章。如果你也喜歡Kotlin,歡迎加入我們~~~

Kotlin系列文章,歡迎檢視:

資料結構與演算法系列:

Kotlin 原創系列:

Effective Kotlin翻譯系列

翻譯系列:

實戰系列:

相關文章