單例模式之Java和Kotlin版
前段時間在封裝Kotlin版的MVP+RxJava+Retrofit專案框架的時候,需要多處使用單例模式,但是Kotlin的單例模式卻是接觸不夠的,所以筆者抽空學習了一波。對照著Java版,總結了常用的五大單例模式,分別為:餓漢模式 、懶漢模式(執行緒安全版、執行緒不安全版) 、雙重檢測模式 、靜態內部類模式 。下面我將一一分析下這五種模式的寫法和優缺點,都是對比著Java版分析的。
這裡大致解釋下單例模式是什麼以及怎麼用。筆者認為:從Android
開發的角度來說,單例模式也就是在這個系統中只有一個例項,並且這個例項是可以全域性訪問的。好比如封裝了一個網路請求類HttpUtil
,我們不想每次使用它的時候都new
一個它的例項,這樣請求一個網路就生成了一個它的物件,在記憶體和執行緒安全上來說是非常不理想的。我們期待的結果是所有網路請求共用一個它的例項,只需呼叫它的方法就可以請求網路,在整個系統中只存在一個HttpUtil
的例項,這才是我們想要的結果。
下面我將一一介紹這五種常用的單例模式:
餓漢模式:
先回顧下Java的餓漢模式寫法:
Java版:
public class Singleton { //餓漢模式 private static Singleton mInstance = new Singleton(); public Singleton() { } public static Singleton getInstance() { return mInstance; } }
餓漢模式的寫法應該是最簡單的,在類載入的時候就初始化了單例物件,然後在getInstance()
靜態方法中返回。接著我們看看Kotlin 的餓漢模式是什麼樣的。
Kotlin版:
object MySingleton{ fun printMsg() = System.out.print("MySingleton") } // 呼叫 MySingleton.printMsg()
Kotlin的餓漢模式乍一看好像沒程式碼,其實Kotlin中的object
就聲明瞭一個類為餓漢模式的單例,經過object
修飾過得類就是一個靜態類,預設實現了餓漢模式。
雖然餓漢模式在兩種語言中都是很簡單的寫法,但是餓漢模式存在著一個缺點:類載入慢。
懶漢模式:執行緒不安全
Java版:
public class Singleton { private static Singleton instance = null; public Singleton() { } public static Singleton getInstance() { if (null == instance) { instance = new Singleton(); } return instance; } }
Java的懶漢模式(執行緒不安全)版就是在類載入時候宣告一個類物件,然後在靜態方法getInstance()
中判斷一次是否為空,如果為空就new
出它的例項,不為空直接返回這個物件。下面是Kotlin版的懶漢模式(執行緒不安全)版:
Kotlin版:
class MySingleton private constructor() { companion object { val mInstance by lazy(mode = LazyThreadSafetyMode.NONE) { MySingleton() } } fun printMsg() = System.out.print("MySingleton") } // 呼叫 MySingleton.mInstance.printMsg()
以上是kotlin預設懶漢寫法,下面是自己手寫和Java類似:
class MySingleton private constructor() { companion object { private var mInstance: MySingleton? = null fun getInstance(): MySingleton { if (mInstance == null) { mInstance = MySingleton() } return mInstance!! } } fun printMsg() = System.out.print("MySingleton") } // 呼叫 MySingleton.getInstance().printMsg()
可以看到Kotlin有兩種寫法,一種是Kotlin推薦的,一種是依照Java自己寫的。Kotlin推薦採用companion object
來宣告一個靜態變數或者靜態方法,等同於Java的static
,然後通過by lazy
來宣告一個單例。
為什麼說這種寫法是執行緒不安全的呢?我們仔細想想,在getInstance()
方法中,只有一次判斷是否為空,如果在mInstance
未初始化的情況下,兩次或者多次呼叫這個方法,那麼都會通過if (mInstance == null)
這個判斷,造成兩次或多次new
出MySingleton
物件,顯然這個不滿足我們心中的單例。
接著來看看改良版的懶漢模式,安全版:
懶漢模式:執行緒安全
Java版:
public class SingletonSecurity { private static SingletonSecurity mInstance = null; public SingletonSecurity() { } public static synchronized SingletonSecurity getInstance() { if (null == mInstance) { mInstance = new SingletonSecurity(); } return mInstance; } }
Java版安全版是通過synchronized
關鍵字給這個方法上鎖,那麼我們來看看Kotlin是否也是通過鎖機制來實現的。
Kotlin版:
class MySingleton private constructor() { companion object { private var mInstance: MySingleton? = null @Synchronized fun getInstance(): MySingleton { if (mInstance == null) { mInstance = MySingleton() } return mInstance!! } } fun printMsg() = System.out.print("MySingleton") }
沒錯,Kotlin也是採用了和Java一樣的機制,通過@Synchronized
來給getInstance()
方法上鎖,這就是兩種語言的懶漢模式(安全版)。一次只能有一個執行緒進入該方法,其它執行緒要想進入該方法,不好意思,只能在外面排隊等候,當前執行緒執行完該方法後,別的執行緒才能進入。
這種單例雖然保證了執行緒的安全,但是每次進入getInstance()
方法的時候都需要進行同步,造成不必要的同步開銷。
雙重檢測模式:
Java版:
public class SingletonSecurity { private volatile static SingletonSecurity mInstance = null; public SingletonSecurity() { } public static SingletonSecurity getmInstance() { if (null == mInstance) { synchronized (SingletonSecurity.class) { if (null == mInstance) { mInstance = new SingletonSecurity(); } } } return mInstance; } }
Java版的雙重檢測模式採用了兩次為空判斷,並且在宣告變數的時候用到了volatile
關鍵字,如果不瞭解volatile
關鍵字的可以閱讀望舒大大的ofollow,noindex">Java併發程式設計(三)volatile域
這篇文章,非常詳細,很容易理解。
再來看看Kotlin是否也是如此:
Kotlin版:
class MySingleton private constructor() { companion object { val mInstance by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED){ MySingleton() } } fun printMsg() = System.out.print("MySingleton") }
這種是Kotlin推薦的通過by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED)
來建立一個雙重檢測的單例模式,和懶漢模式(執行緒不安全)版類似,只是mode
不一樣了,大家要注意區分。
類似Java寫法
class MySingleton private constructor() { companion object { @Volatile private var mInstance: MySingleton? = null fun getInstance(): MySingleton { if (mInstance == null) { synchronized(MySingleton::class) { if (mInstance == null) { mInstance = MySingleton() } } } return mInstance!! } } fun printMsg() = System.out.print("MySingleton") }
這是筆者依照Java版的寫法實現的雙重檢測模式,寫法和Java基本一致。
兩次為空判斷,第一次是為了不必要的同步,第二次是在mInstance
為空的時候才建立例項。這種單例模式解決上面執行緒不安全、多與同步的問題,也是用的比較多的一種寫法。
靜態內部類單例模式:
Java版:
public class SingletonSecurity { public SingletonSecurity() { } public static SingletonSecurity getInstance() { return SingletonHolder.instance; } private static class SingletonHolder { private static final SingletonSecurity instance = new SingletonSecurity(); } }
這是Java的靜態內部類單例寫法,對這種寫法放在Kotlin寫法後面。
Kotlin版:
class MySingleton private constructor() { companion object { fun getInstance() = SingleHolder.mInstance } object SingleHolder { val mInstance: MySingleton = MySingleton() } fun printMsg() = System.out.print("MySingleton") }
大家可以看到,在類載入的時候並沒有宣告mInstance
變數,只有第一次呼叫getInstance()
方法時,才會去SingleHolder
類中取mInstance
物件,在這個時候SingleHolder
就會去建立一個mInstance
例項,並且是靜態的,只建立一次,全域性共用。
這種方法是推薦替代雙重檢測模式的一種模式,在記憶體優化上、安全性上都得到了很好的保障。