女朋友驚掉下巴問我:單例模式竟有七種寫法?
前言
接下來,我們要進入的是設(shè)計模式篇,關(guān)于設(shè)計模式,作為程序員的你,肯定在工作中或者面試中遇到過很多次了吧
記得當時18年上大三的時候出去找實習,也問過了解哪些設(shè)計模式,不過我個人回答的最多的最詳細的大概也就是單例模式了,因為我覺得這個應(yīng)該是最最好理解的了,雖然有很多種寫法,這是為了解決不同環(huán)境下的不同問題,當時我應(yīng)該是把懶漢、餓漢直接都手撕了一遍,也簡單的把懶漢和餓漢的區(qū)別說了說
當時令我吃驚的是面試官告訴我,單例模式其實有七種寫法,甚至可以更多,我當時驚得下巴都掉了,當時我就感覺到了這個行業(yè)滿滿的挑戰(zhàn)和滿滿的知識等著我學(xué)習
果不其然,現(xiàn)在越學(xué)越覺得自己廢物,越學(xué)越感覺自己有太多不會的了,不過這個路肯定還是要走下去的,撥開云霧見天明,堅持下去吧
接下來我們來簡單介紹下單例模式
單例模式,顧名思義,就是唯一的實例。在當前進程中,有且只有一個單例模式創(chuàng)建的類對象
比如生活中的太陽、只能有一個吧,所以只能有一個實例,這個例子要是用在當年后羿射箭之前不合適,但是現(xiàn)在應(yīng)該還算是合適的吧
再比如寫一個校園管理系統(tǒng),有一個校長的角色,只能有一個,這個對象在該系統(tǒng)中做成單例就比較合適(其余的是副校長的 親
這個模式應(yīng)該是大家最常見的,也是大家認為最簡單的了吧,但是實際上這個模式里面還是有很多細節(jié)的,也有很多的點值得大家思考的,待會咱們一起看各種寫法的時候大家記得帶著你的思考和你的問題去學(xué)習
正文
單例模式特點
單例模式有如下的特點:
1、一個JVM中有且只有一個實例的存在,構(gòu)造器私有,外部無法創(chuàng)建該實例
2、提供一個公開的get方法獲得唯一的這個實例
有哪些優(yōu)點呢:
1、省去了new的操作,降低系統(tǒng)內(nèi)存的使用頻率,減輕GC的壓力
2、系統(tǒng)中的一些類需要全局單例,比如spring中的controller,再比如人類的太陽
3、避免了資源的重復(fù)的占用,減少了內(nèi)存的開銷
其實也是有一些缺點的:
沒有接口,不可繼承與單一職責原則沖突,一個類應(yīng)該只關(guān)心內(nèi)部邏輯,而不關(guān)心外面怎么樣來實例化
先把要介紹的七種給大家說一下,大家有個印象
餓漢式、懶漢式線程不安全和安全版、DCL雙重檢測鎖模式的線程不安全和安全版、靜態(tài)內(nèi)部類、枚舉類
大家先聽個耳熟,下面一一介紹
餓漢式
餓漢式,就是比較餓,于是乎吃的比較早,也就是創(chuàng)建的比較早,會隨著JVM的啟動而初始化該單例
也正是由于這種類裝載的時候就完成了單例的實例化了,不存在所謂的線程安全問題,是線程安全的,相應(yīng)的缺點就是未達到lazy loading的效果,如果創(chuàng)建的這個單例類始終未用到,便回造成資源浪費
其實在實際開發(fā)中,即使知道一定用得到,我們一般也不太會使用這種機制,因為如果單例對象很多,會影響啟動的速度,采用懶加載機制是比較節(jié)約資源的
開發(fā)中很多思想也是采用懶加載,只有當真正用到一個東西的時候才允許它占用相應(yīng)的資源
- /**
- * 餓漢式:通過classloader機制避免了多線程的同步問題,在類裝載的時候完成實例化
- * 優(yōu)點:寫法簡單,類裝載的時候完成實例化,避免了線程同步的問題
- * 缺點:未達到lazy loading的效果,如果始終未用到則可能造成資源浪費
- * 適用場景:
- */
- public class HungrySingleton {
- //1、構(gòu)造器私有化
- private HungrySingleton(){}
- //2、類的內(nèi)部創(chuàng)建對象的實例
- private final static HungrySingleton dayu = new HungrySingleton();
- //3、將類的內(nèi)部實例提供一個靜態(tài)方法返回出去
- private static HungrySingleton getInstance(){
- return dayu;
- }
- }
懶漢式(線程不安全、線程安全)
懶漢式咯,就是比較懶,在啟動的時候,不會進行該單例對象的創(chuàng)建,只有當真正用到的時候才會去加載這些東西
之所以加懶漢式,大概就是采用了懶加載思想
我們看下面這個懶漢式的代碼
- /**
- * 懶漢式
- * 缺點:線程不安全,工作中一般不用
- */
- public class NotSafeLazySingleton {
- //構(gòu)造器私有化
- private NotSafeLazySingleton(){}
- //暫時不加載實例
- private static NotSafeLazySingleton dayu;
- /**
- * 存在線程安全問題
- * 線程A到括號dayu == null判斷完之后,進入括號內(nèi)部,
- * 此時線程B獲得執(zhí)行權(quán),判斷==null也是true,所以也進入
- * 此時兩個線程便出現(xiàn)了兩個dayu對象
- * @return
- */
- public static NotSafeLazySingleton getInstance(){
- if(dayu == null){
- dayu = new NotSafeLazySingleton();
- }
- return dayu;
- }
- }
其實有過多線程的經(jīng)驗的小伙伴應(yīng)該很快就看出來了,上面這種懶漢式是有線程安全問題的,當線程A執(zhí)行到if(dayu == null)這一行的時候,判斷為空,true進入括號內(nèi)部,此時線程A的時間片用完了,到了線程B的執(zhí)行了,于是乎也會判斷為空,進入括號內(nèi)部
線程B創(chuàng)建了一個NotSafeLazySingleton對象,輪到線程A執(zhí)行的時候,由于在之前已經(jīng)判斷完進入了括號內(nèi)部,于是線程A也會創(chuàng)建一個NotSafeLazySingleton對象
GG,這樣不是我們想要的效果,這就不屬于單例模式了,所以這種在多線程情況下是存在安全問題的
有了問題,自然就是解決咯,可能有的小伙伴也想到了,存在線程安全問題,那就加上線程安全關(guān)鍵字synchronized來解決,于是乎便有了下面的代碼,我們給函數(shù)加上關(guān)鍵字synchronized,但是這樣會造成效率極其低下
所有調(diào)用這個方法去使用單例對象的地方都需要排隊阻塞知道該鎖的釋放,在多線程情況下會迅速降低效率
- /**
- * 懶漢式安全寫法
- * 缺點:Synchronized關(guān)鍵字導(dǎo)致方法效率低 效率極低
- * 優(yōu)點:線程安全
- * 適用場景:實際開發(fā) 不推薦使用
- */
- public class SafeLazySingleton {
- //構(gòu)造器私有化
- private SafeLazySingleton(){}
- //暫時不加載實例
- private static SafeLazySingleton dayu;
- /**
- * synchronized導(dǎo)致所有通過該方法獲取該對象的時候都要排隊
- */
- public static synchronized SafeLazySingleton getInstance(){
- if(dayu == null){
- dayu = new SafeLazySingleton();
- }
- return dayu;
- }
- }
所有調(diào)用這個方法去使用單例對象的地方都需要排隊阻塞知道該鎖的釋放,在多線程情況下會迅速降低效率,于是有了下面的這種改進方法
只鎖其中的部分代碼,看下下面的代碼
- /**
- * 本意上是對SafeLazySingelton的改進 因為前面的對整個方法進行加鎖的效率實在是太低了
- * 但是這種還是不能起到線程同步的作用 和NotSafeLazySingelton類似 只要線程進入了== null的里面
- * 此時另一個線程獲得CPU分配的時間片 則會出現(xiàn)多個對象
- */
- public class NotSafeLaySingleton2 {
- //構(gòu)造器私有化
- private NotSafeLaySingleton2(){}
- //暫時不加載實例
- private static NotSafeLaySingleton2 dayu;
- /**
- * @return
- */
- public static NotSafeLaySingleton2 getInstance(){
- if(dayu == null){
- synchronized (NotSafeLaySingleton2.class){
- dayu = new NotSafeLaySingleton2();
- }
- }
- return dayu;
- }
- }
上面的這種代碼看著有問題嗎?
不知道你認真讀了上面代碼之后,內(nèi)心是怎么想的,聰明的小伙伴已經(jīng)發(fā)現(xiàn)了事情不是這么簡單,發(fā)現(xiàn)其中了問題
是的,上面的這種改進方法,貌似實現(xiàn)了效率跟高些,但是會隨之帶來多線程的問題
線程A判斷dayu == null進入括號,還沒拿到NotSafeLaySingleton2的鎖,時間片消耗完了,此時線程B也判斷,發(fā)現(xiàn)dayu == null也成立,此時也會進入括號,假設(shè)線程B拿到了鎖,創(chuàng)建了一個NotSafeLaySingleton2對象,執(zhí)行完之后釋放鎖。線程A拿到該鎖,會重新創(chuàng)建一個對象,于是出現(xiàn)多例現(xiàn)象
先是通過synchronized加在方法層面解決并發(fā)問題,但是隨之而來帶來效率問題,于是為了提高效率,加在內(nèi)部,但是加在內(nèi)部就有了相應(yīng)的線程安全問題
說了這么多,就是要引出我們下面的線程安全的DCL的單例模式
看下怎么寫
雙重檢查鎖模式DCL- double chechked locking(線程安全)
上面那個其實屬于單重檢查鎖模式,我起的名字,因為只檢查了一個地方的鎖,正是如此也帶來了多線程的問題,于是乎就有了下面這種雙重檢測形勢的單例模式了,一起看看吧,穩(wěn)得一批
- /**
- * 雙重檢測單例:穩(wěn)得一批
- * 優(yōu)點:線程安全 延遲加載 效率相對來說也不錯
- *使用場景:實際開發(fā)中 用的比較多
- */
- public class DoubleCheckSingleton {
- private static volatile DoubleCheckSingleton dayu;
- private DoubleCheckSingleton(){}
- /**
- * 解決線程安全的問題同時 也解決懶加載問題
- * @return
- */
- public static DoubleCheckSingleton getInstance(){
- if(dayu == null){
- synchronized (DoubleCheckSingleton.class){
- if(dayu == null){
- dayu = new DoubleCheckSingleton();
- }
- }
- }
- return dayu;
- }
- }
上面這種在進入了data == null的內(nèi)部也會再次判斷一次是否還等于空,這種就很好的解決了多線程的問題
這種DCL的單例模式在工作中算是常用的一種了,有效的解決高并發(fā)下的單例模式問題
靜態(tài)內(nèi)部類
靜態(tài)內(nèi)部類加載單例,類加載機制保證線程安全,而且還有一個優(yōu)點,懶加載,只有在調(diào)用getInstance的時候才會加載內(nèi)部類,才會創(chuàng)建這個對象
外部類被裝載的時候,內(nèi)部類不會立即被裝載,調(diào)用getInstance才會裝載,并且只會裝載一次,且不存在線程安全問題
- /**
- * 靜態(tài)內(nèi)部類加載單例
- * 優(yōu)點:類裝載機制保證線程安全 懶加載 只有調(diào)用getInstance才會加載內(nèi)部類
- * 適用場景:
- */
- public class StaticInnerClassSingleton {
- private StaticInnerClassSingleton(){}
- /**
- * 1、外部類被裝載時 內(nèi)部不會立即被裝載
- * 2、調(diào)用getInstance方法時會裝載 只會裝載一次 且不存在線程安全
- */
- private static class SingletonInstance{
- private static final StaticInnerClassSingleton dayu = new StaticInnerClassSingleton();
- }
- //返回靜態(tài)內(nèi)部類中的對象
- public static StaticInnerClassSingleton getInstance(){
- return SingletonInstance.dayu;
- }
- }
枚舉類
枚舉類也是可以用作單例模式,而且還很簡單
Effective Java作者Josh Bloch所提倡的單例實現(xiàn)的方式就是這種,這種無線程安全問題,還可以防止反序列化重新創(chuàng)建新的對象
- /**
- * 枚舉實現(xiàn)單例
- * 優(yōu)點:簡潔 無線程安全問題 還可以防止反序列化重新創(chuàng)建新的對象
- * Effective Java作者Josh Bloch提倡的方法
- */
- public class EnumSingleton {
- public static void main(String[] args) {
- //instance和instance2是同一個對象
- Singleton instance = Singleton.INSTANCE;
- Singleton instance2 = Singleton.INSTANCE;
- }
- enum Singleton{
- INSTANCE;
- }
- }
總結(jié)
設(shè)計模式應(yīng)該屬于面試高頻,而單例模式又是設(shè)計模式的最簡單,或者說是最常見的設(shè)計模式之一,看完這篇文章,大家應(yīng)該都知道單例模式的多種寫法了,也知道各種的優(yōu)劣勢和相應(yīng)的使用場景了
我們思考一個問題,為什么要使用單例模式而使用靜態(tài)方法
這兩個其實都可以實現(xiàn)我們加載的最終目的,但是他們一個是基于對象的,一個是屬于面向?qū)ο蟮?,就像是很多種情況,我們通過普通的編碼也可以實現(xiàn),但是我們引入設(shè)計模式來更好的體現(xiàn)編程思想
如果一個方法和他所在的類的實例對象確實是無關(guān)的,那么它就應(yīng)該是靜態(tài)的,反之它就應(yīng)該是非靜態(tài)的,如果我們需要使用非靜態(tài)的方法,但是在創(chuàng)建類對象的時候,又只需要維護一個實例,不想創(chuàng)建多個不同的實例,就需要使用單例模式了。