iT邦幫忙

2022 iThome 鐵人賽

DAY 25
0
Software Development

Kotlin on the way系列 第 25

Day 25 設計模式 單例模式的細節 Design pattern - Singleton Creational pattern

  • 分享至 

  • xImage
  •  

設計模式,重要嗎?

我擺在這麼後面的理由,就是因為他不是最重要的,甚至是說,寧願你把程式碼先寫好,也不用先去學設計模式

當你先去學設計模式,你可能會硬套進現有專案,變成有設計模式的糙 code,但相反的,如果你先把程式寫好,你就已經自然地學會了幾種設計模式了

那回到我們主題上,設計模式主要可以分成三類,建構、結構以及行為,礙於篇幅,我只能每個類型分享幾種常見的實作,以及注意事項,在同個平台上,我推薦的文章是 什麼?又是/不只是 Design Patterns!?

前情提要,我被讀者制止了一次講三類設計模式XD

structure

  • Singleton
    • Eagerly Initialized
    • Lazy Initialized
    • Thread safe Lazy Initialized
    • Double-Checked Locking
    • Static inner class
    • enum

單例 Singelton

單例,這是最好入門、最方便、最常誤用也最多人愛分享的設計模式了,我相信每篇文章都有其優點,但大家記得多看多比較,去了解比較全面的知識呀

首先,單例模式是個 anti-pattern ,如果一篇文章只說單例的優點,別看了不值得,但相對的,你也絕對不是直接不學單例,單例有其優劣,請真正了解後,再決定是否使用他

我們先回到單例模式本身,這個設計模式有兩個特點

  1. 一個類別只會有一個實例
  2. 提供一個全域訪問點
  3. 建構函式是私有的,使其無法由外部建構
  4. 用其私有的靜態變數紀錄實例
  5. 提供公開的靜態方法,使外部可以訪問

而 Kotlin 裡面,更是提供了最簡短的方式建立單例,語法如下

object SingleObj {
    
}

在 Kotlin 語法裡,還可以用可見度修飾符限制可見度,像是 private object SingleObj {}

阿,這就完了?要是只學到這邊,就別說你會單例模式了拜託

單例模式還需要注意幾個問題

  1. 建立時,是否執行序安全
  2. 是否要延遲建立
  3. 存取實例時,是否需要加鎖

現在我們先撇開語法糖,來看看幾種實現單例的方式

  1. Eagerly Initialized
    在類別載入時,就已經確保有實例了
    在 Java 裡面,寫法是這樣
public class SingleObj {

    private SingleObj(){}//私有構造函式
    private static SingleObj instance = new SingleObj();//用其私有的靜態變數紀錄實例

    public static SingleObj getInstance(){//對外公開外部訪問方法
        return instance;
    }
}

現在來看看下面這段

public final class SingleObj {
   @NotNull
   public static final SingleObj INSTANCE;//用其私有的靜態變數紀錄實例

   private SingleObj() { //私有構造函式
   }

   static {
      SingleObj var0 = new SingleObj();//對外公開外部訪問方法
      INSTANCE = var0;
   }
}

//其實就是 Kotlin
object SingleObj {
    
}

看完是不是覺得效果一樣呀,第一個是 Java 程式,第二的是 Kotlin object 反組譯的程式,沒錯,用 Object 做單例幫我們省下了那麼多代碼,這種實現方式當然也有其優劣

  • pros
    • 建立實體是執行序安全的
    • 存取時不用加鎖
  • cons
    • 不是延遲載入,是程序一開始就會初始化
  1. Lazy Initialized
    如果我們希望應用能盡快載入,就不會選擇第一種方式,而是選擇其他延遲載入的設計,比如這個
class SingleObj private constructor() {
    companion object {
        private var instance: SingleObj? = null
            get() {
                if (field == null) {
                    field = SingleObj()
                }
                return field
            }
        fun get(): SingleObj{
         return instance!!
        }
    }
}
  • pros
    • 可以延遲載入
  • cons
    • 執行緒不安全
  1. Thread safe Lazy Initialized
    前面的實現方式,儘管可以提供延遲載入的特性,但在多執行序的情境下並不安全,可以會有多個執行序同時使用到 instance ,使兩邊的 if 條件都成立,而我們也能基於這個問題做改良,並產生新的問題
class SingleObj private constructor() {
    companion object {
        private var instance: SingleObj? = null
            get() {
                if (field == null) {
                    field = SingleObj()
                }
                return field
            }
        @Synchronized
        fun get(): SingleObj{
         return instance!!
        }
    }
}
  • pros
    • 執行序安全
    • 可以延遲載入
  • cons
    • 執行緒被上鎖了,併發操作時會影響效率
      • 視讀取情況,可能無傷大雅XD
  1. Double-Checked Locking
    上面的兩種方式,都有其缺點,而雙重檢測的單例,就能完美的解決這些問題,搭配上委託,給人一種阿斯舒爽的感覺
class SingleObj private constructor() {
    companion object {
        val instance: SingleObj by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
        SingleObj() }
    }
}

稍微一挖,就能找到這邊,這邊就三行程式是重點,4, 11, 16

Volatile 直譯是可揮發的,解決了可見性問題,加了這個關鍵字的變數,任何一個執行緒對他的修改,都會讓其他cpu快取記憶體的值過期,這樣就必須重新去記憶體拿最新的值,更細節看這

if (_v1 !== UNINITIALIZED_VALUE) 判斷是否已經有實例
synchronized(lock) 保證函式或程式區塊執行時,同一時刻只能有一個方法到 critical section ,並保證共享變數的內存可見性

//LazyJVM.kt
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
                }
            }
        }

    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)
}
  • pros
    • 建立時執行序安全
    • 可以延遲建立
    • 僅建立時加鎖,讀取時不需加鎖
  1. Static inner class
    靜態內部類別只有在 instance 被呼叫時才會建立,換句話說,只有執行到 SingleObj.instance 時,才會執行s=SingleObj() 產生實體
class SingleObj private constructor() {
    companion object {
        val instance = SingletonHolder.holder
    }

    private object SingletonHolder {
        val holder= SingleObj()
    }

}

  • pros
    • 建立時執行序安全
    • 可以延遲建立
    • 僅建立時加鎖,讀取時不需加鎖
  1. Enum Singleton
    最後一個是比較特別的, enum 的單例
  • pros
    • 保證執行序安全
    • 支持序列化和反序列化
  • cons
    • 無法編寫複雜邏輯

以上,還只是單例的冰山一角,理解了作法,來了解下為什麼有人說單例是 anti pattern?

  1. 耦合
  2. 難以測試
  3. 多個職責
    其實看看,大概就剩下 global state 比較無解,耦合可以為其做設計,甚至是用 di + interface,職責倒也還好,身兼原本職責和建構自己對外部影響不大

Global State 導致測試上的困難
但這個可能導致的事,不同引用對其造成時序上的改變,而導致測試時無法重現狀態,簡單說 A 類別改了東西, B,C,D 一起爆掉

這問題就去看 Mutability 是把雙面刃 Mutability is double edged sword

參考

https://cloud.tencent.com/developer/article/1803321
https://iter01.com/78974.html
https://iter01.com/573847.html

English

Design pattern, does it matter?

I put this topic in the end of this series, because it is not the most important technical, I rather you write good code first, than learn bunch of design pattern

When you learn design pattern, you might use it improperly, come out with shit design pattern code, on the opposite, if you wrote code with good habit, you will learn design pattern like free

Well, back to the topic, there are three kind of design pattern, build, structure and behavior, I will introduce some of common implement and its detail, in itHome I recommand this series article 什麼?又是/不只是 Design Patterns!?

article structure

  • Singleton
    • Eagerly Initialized
    • Lazy Initialized
    • Thread safe Lazy Initialized
    • Double-Checked Locking
    • Static inner class
    • enum

Singelton

Singleton, the most entry-level, convenience and misused design pattern,

First, Singleton is an anti-pattern, if you read an article only tell you advantage of singleton, don't read it, on the contract, you should actually understand singleton the decide whatever to use it

Back to the singleton pattern, here is some detail of it

  1. a class only have an instance
  2. with a global access point
  3. constructor function is private, it can't create from outside
  4. use private static variable to recode instance
  5. provide public static method, so it can access from outside

In Kotlin, we provide more brief syntax to build singleton

object SingleObj {
    
}

With Kotlin syntax, we can use visibility modifier to limited it private object SingleObj {}

does it finish? If that is all you know, please don't tell others you understand singleton

there are following question in singleton

  1. did you make sure the thread safety when the class constructed
  2. do you need the construct lately
  3. do you need lock the thread when you access it

Now let's take a look with Kotlin syntax sugar

  1. Eagerly Initialized
    it make sure the instance is exists when the class loaded, looks like this in Java
public class SingleObj {

    private SingleObj(){}//private constructor method
    private static SingleObj instance = new SingleObj();// use private static variable record instance

    public static SingleObj getInstance(){//publish access methods 
        return instance;
    }
}

now check the code block

public final class SingleObj {
   @NotNull
   public static final SingleObj INSTANCE;//use private static variable record instance

   private SingleObj() {//private constructor method
   }

   static {
      SingleObj var0 = new SingleObj();///publish access methods 
      INSTANCE = var0;
   }
}

//actually it is  Kotlin
object SingleObj {
    
}

Do you feel it look the same, first one is Java code, the second one is decompiled Kotlin object, indeed the object keyword save us lots of boilerplate code, but what it the pros and cons

  • pros
    • thread safety is guaranteed when construct instance
    • no need to add lock when access it
  • cons
    • not lazy initialize, it will build instance when application start
  1. Lazy Initialized
    If we want the application initialized ASAP, we might choose other design like this one
class SingleObj private constructor() {
    companion object {
        private var instance: SingleObj? = null
            get() {
                if (field == null) {
                    field = SingleObj()
                }
                return field
            }
        fun get(): SingleObj{
         return instance!!
        }
    }
}
  • pros
    • class instance initialized lately
  • cons
    • not thread safe
  1. Thread safe Lazy Initialized
    Although previous implement provide late init, but it is not thread safe, it is possible facing issue when multiple thread using instance, and if condition in both thread also success, therefore we can make the implement better
class SingleObj private constructor() {
    companion object {
        private var instance: SingleObj? = null
            get() {
                if (field == null) {
                    field = SingleObj()
                }
                return field
            }
        @Synchronized
        fun get(): SingleObj{
         return instance!!
        }
    }
}
  • pros
    • thread safe
    • late init
  • cons
    • locked the thread, it might affect efficiency in concurrency operator
      • depends on situation, this might be a acceptable solution
  1. Double-Checked Locking
    Those previous solution has its own disadvantage, but singleton with double check, can perfectly fix those issue, and in Kotlin, we can even use it with delegate
class SingleObj private constructor() {
    companion object {
        val instance: SingleObj by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
        SingleObj() }
    }
}

if you dig deep, you will find those code scope, the ley line is 4, 11, 16

Volatile fix the visibility issue, any variable with this keyword, whatever thread modify this variable, will expire the value in other cpu cache memory, so other thread have to get the newest value check out more detail

if (_v1 !== UNINITIALIZED_VALUE) check does the instance exists
synchronized(lock) guarantee function or program only one client access critical section in same time, and it also make sure the memory visibility

//LazyJVM.kt
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) {//if have instance, return 
                @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// record the instance
                    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)
}
  • pros
    • thrad safe
    • late init
    • add lock while construct, no lock when access it
  1. Static inner class

Static inner class only construct while other program call it, in the other words, only initialize when execute SingleObj,instance it will run s=SingleObj()

class SingleObj private constructor() {
    companion object {
        val instance = SingletonHolder.holder
    }

    private object SingletonHolder {
        val holder= SingleObj()
    }

}

  • pros
    • thrad safe
    • late init
    • add lock while construct, no lock when access it
  1. Enum Singleton
    Last one is special, singleton with enum
  • pros
    • thread safe
    • support serialization and deserialization
  • cons
    • can't apply complicit logic

Reference

https://cloud.tencent.com/developer/article/1803321
https://iter01.com/78974.html
https://iter01.com/573847.html


上一篇
Day 24 KMM 和整潔架構 KMM and Clean architecture
下一篇
Day 26 設計模式 工廠建構的細節
系列文
Kotlin on the way31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言