新聞中心
一、序

大家好,這里是承香墨影。
單例模式我相信大家應(yīng)該不會陌生,隨手抓一個(gè)程序員,讓他說說最常用的 3 種設(shè)計(jì)模式,其中一定包含單例模式。
單例最重要的是,關(guān)注唯一性以及線程安全問題。而在 Java 中,單例存在多種實(shí)現(xiàn)范式,例如:餓漢式、懶漢式、靜態(tài)內(nèi)部類、雙重檢測等等,甚至還可以利用枚舉的特性實(shí)現(xiàn)單例,可謂是把單例玩出了花樣。
這其中,餓漢式單例實(shí)現(xiàn)代碼是最簡單的,關(guān)鍵代碼只需一行 static final 申明對象即可,代碼簡單且滿足需求。
但是餓漢式經(jīng)常會被我們"嫌棄",日常 Review Code 時(shí),甚至看到餓漢式單例也會「友善的建議」對方使用雙重檢測。
餓漢式依賴 JVM 加載類的時(shí)機(jī),來完成靜態(tài)對象的初始化,這個(gè)過程本身就是線程安全的。而它最被人詬病的,其實(shí)是無法延遲加載,完全依賴 JVM 加載類的時(shí)機(jī),這就導(dǎo)致單例類加載時(shí)機(jī)不可控。也就有可能,有些資源,業(yè)務(wù)還未使用,單例類就已經(jīng)準(zhǔn)備好了,導(dǎo)致過多的占用了系統(tǒng)資源。
我們再回過頭來看看 Kotlin。在 Kotlin 中,實(shí)現(xiàn)單例非常簡單,只需要將關(guān)鍵字 class 替換為 object 即可。
- object SomeSingleton{
- fun sayHi(){}
- }
但 Kotlin 的 object 其實(shí)就是餓漢式單例。它難道不怕存在資源占用的問題嗎?
二、Kotliin 的 object
2.1 Kotlin 的 object 原理
在開始 Kotlin 的 object 選擇餓漢式單例前,我們先來看看 Kotlin object 原理。
Kotlin 和 Java 可以互相調(diào)用,Kotlin 代碼運(yùn)行前也會被編譯器編譯成 Java 字節(jié)碼。那我們就可以通過工具將其還原為 Java 代碼進(jìn)行分析。
這個(gè)轉(zhuǎn)換工具, AS 原生支持。借助 AS 的 Tools → Kotlin → Show Kotlin Bytecode,就可以查看 Kotlin 編譯后的 Java 字節(jié)碼,再點(diǎn)擊 Decompile 按鈕,就可以將字節(jié)碼轉(zhuǎn)成 Java 代碼。
可以看到,INSTANCE 使用 static final 聲明,并且在 static 代碼塊內(nèi)對其進(jìn)行初始化,標(biāo)準(zhǔn)的餓漢式單例。
2.2 餓漢式如何保證唯一和線程安全?
前面提到,單例最重要的就是關(guān)注其唯一性和線程問題。
需要在任何情況下,都確保一個(gè)類只存在一個(gè)實(shí)例,不會因?yàn)槎嗑€程的訪問,導(dǎo)致創(chuàng)建多個(gè)實(shí)例。同時(shí)也不會因?yàn)槎嗑€程而引入新的效率問題。
餓漢式單例的原理,其實(shí)是基于 JVM 的類加載機(jī)制來保證其符合單例的規(guī)范的。
簡單來說,JVM 在加載類的時(shí)候,會經(jīng)過初始化階段(即 Class 被加載后,且被線程使用前)。在初始化期間,JVM 會獲取一把鎖,這個(gè)鎖可以同步多個(gè)線程,對一個(gè)類的初始化,確保只有一個(gè)線程完成類的加載過程。這個(gè)步驟是線程安全的。
上圖很清晰的描述了類的初始化鎖工作流程,這里就不展開細(xì)說。
三、所謂的餓漢式問題
前文提到,餓漢式單例最被人詬病的問題,在于無法實(shí)現(xiàn)懶加載,完全依賴虛擬機(jī)加載類的策略加載。
3.1 懶加載
懶加載的目的,說白了就是為了避免,無必要的資源浪費(fèi),在不需要的時(shí)候不加載,等什么時(shí)候業(yè)務(wù)真的需要使用到它的時(shí)候,再加載資源。
雖然餓漢式依賴虛擬機(jī)加載類的策略,但虛擬機(jī)本身也會有優(yōu)化項(xiàng),那就是「按需加載」的策略。
虛擬機(jī)在運(yùn)行程序時(shí),并不時(shí)在啟動時(shí),就將所有的類都加載并初始化完成,而是采用「按需加載」的策略,在真正使用時(shí),才會進(jìn)行初始化。
例如 顯式的 new Class()、調(diào)用類的靜態(tài)方法、反射、Class.forName() 等,這些事件首次發(fā)生時(shí),都會觸發(fā)虛擬機(jī)加載類。
例如前文中,SomeSingleton 這個(gè)單例類,我們放到一個(gè) App 中運(yùn)行一下,App 先啟動,點(diǎn)擊按鈕執(zhí)行 SomeSingleton.sayHi() 方法。
- 15:39:34.539 I/cxmyDev: App running
- 15:39:44.606 I/cxmyDev: SomeSingleton init
- 15:39:44.606 I/cxmyDev: SomeSingleton sayHi
注意 Log 的時(shí)間,只有點(diǎn)擊按鈕執(zhí)行 SomeSingleton.sayHi() 時(shí),該單例類才被虛擬機(jī)加載。
也就是說,通常只有在你真實(shí)使用這個(gè)類時(shí),它才會真的被虛擬機(jī)初始化,我們并不需要擔(dān)心會被提前加載而導(dǎo)致資源浪費(fèi)。
當(dāng)然,不同虛擬機(jī)的實(shí)現(xiàn)方式不同,這并不是強(qiáng)制的,但是大多數(shù)為了性能都會準(zhǔn)守此規(guī)則。
3.2 軟件設(shè)計(jì)的角度
既然餓漢式的單例,也是在首次使用時(shí)初始化,這自然就是一種類懶加載的效果。
那我們再換個(gè)角度思考,如果餓漢式單例就是在程序啟動時(shí),就初始化好了,有問題嗎?
在 Java 中,其實(shí)構(gòu)造一個(gè)普通對象的成本很低。那為什么到了單例模式下,就覺得是個(gè)問題呢?
主要是單例的生命周期較長,承載了業(yè)務(wù)和狀態(tài),我們不提前構(gòu)造無非是 2 個(gè)問題。
- 單例對象本身,初始化比較復(fù)雜或耗時(shí),提前初始化會影響其他業(yè)務(wù);
- 單例初始化后,持有的資源太多,導(dǎo)致內(nèi)存資源的浪費(fèi);
問題一:初始化邏輯復(fù)雜
如果單例在初始化階段,存在大量的邏輯,那么也不應(yīng)該等到需要使用時(shí)才初始化它,否者必然會影響到接下來的業(yè)務(wù)性能。而是應(yīng)該在此之前,系統(tǒng)較為空閑時(shí)初始化。
例如 Android 下就可以借助 IdleHandler 在空閑時(shí)提前做一些初始化工作。
問題二:持有資源太多
系統(tǒng)的各項(xiàng)資源,從來就沒有夠的時(shí)候。
任何時(shí)候緩存和性能都是要平衡的,單例作為一個(gè)生命周期較長的類,更不應(yīng)該長時(shí)間持有大量的資源。否者就算加載時(shí)不報(bào)錯(cuò),也必然會埋下 OOM 隱患,是之后內(nèi)存優(yōu)化時(shí),重點(diǎn)關(guān)注的對象。
在編寫代碼時(shí),就思考對內(nèi)存資源的合理利用,而不是等到內(nèi)存問題嚴(yán)重時(shí),再集中進(jìn)行內(nèi)存優(yōu)化。合理使用弱引用優(yōu)化持有資源,也是一種不錯(cuò)的優(yōu)化手段。
另外如果初始化時(shí),就是必須會占用一些資源,那么基于 Fail-fast 原則,有問題也應(yīng)該盡早的暴露出來。
畢竟 App 崩潰在開發(fā)手里,這叫問題,而崩潰在用戶手里,這就叫事故。
四、小結(jié)
時(shí)刻今天我們聊了 Java 的單例,以及 Kotlin object 單例的實(shí)現(xiàn)原理,最后我們再小結(jié)一下。
Kotlin object 使用「餓漢式」單例,依賴 JVM 的類加載機(jī)制確保唯一和線程安全;
JVM 加載類采用「按需加載」策略,確保懶加載;
Kotlin 的 object 選擇餓漢式單例,在性能和實(shí)現(xiàn)上都不存在問題,使用它無需顧慮。
網(wǎng)站欄目:雙重檢測就比餓漢式高級?那Kotlin的object為什么用餓漢式?
鏈接分享:http://m.fisionsoft.com.cn/article/cojhgoi.html


咨詢
建站咨詢
