偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

一個單例模式,沒必要這么卷吧

開發(fā) 系統(tǒng)
本文從一些簡單的業(yè)務(wù)場景入手,開始著手優(yōu)化系統(tǒng)代碼。那么什么樣的業(yè)務(wù)代碼,動了之后影響最小呢?我們就從泛濫創(chuàng)建的線程池著手,用單例模式做一次重構(gòu)。

老貓的設(shè)計模式專欄已經(jīng)偷偷發(fā)車了。不甘愿做crud boy?看了好幾遍的設(shè)計模式還記不住?那就不要刻意記了,跟上老貓的步伐,在一個個有趣的職場故事中領(lǐng)悟設(shè)計模式的精髓。還等什么?趕緊上車吧

如果把系統(tǒng)軟件比喻成江湖的話,那么設(shè)計原則絕對是OO程序員的武功心法,而設(shè)計模式絕對是招式。光知道心法是沒有用的,還是得配合招式。只有心法招式合二為一,遇到強敵(“坑爹系統(tǒng)”)才能見招拆招,百戰(zhàn)百勝。

故事

之前讓小貓梳理的業(yè)務(wù)流程以及代碼流程基本已經(jīng)梳理完畢【系統(tǒng)梳理大法&代碼梳理大法】。從代碼側(cè)而言也搞清楚了系統(tǒng)臃腫的原因【違背設(shè)計原則】。小貓逐漸步入正軌,他決定從一些簡單的業(yè)務(wù)場景入手,開始著手優(yōu)化系統(tǒng)代碼。那么什么樣的業(yè)務(wù)代碼,動了之后影響最小呢?小貓看了看,打算就從泛濫創(chuàng)建的線程池著手吧,他打算用單例模式做一次重構(gòu)。

在小貓接手的系統(tǒng)中,線程池的創(chuàng)建基本是想在哪個類用多線程就在那個類中直接創(chuàng)建。所以基本上很多service服務(wù)類中都有創(chuàng)建線程池的影子。

寫在前面

遇到上述小貓的這種情況,我們的思路是采用單例模式進行提取公共線程池執(zhí)行器,然后根據(jù)不同的業(yè)務(wù)類型使用工廠模式進行分類管理。

接下來,我們就單例模式開始吧。

概要

單例模式定義

單例模式(Singleton)又叫單態(tài)模式,它出現(xiàn)目的是為了保證一個類在系統(tǒng)中只有一個實例,并提供一個訪問它的全局訪問點。從這點可以看出,單例模式的出現(xiàn)是為了可以保證系統(tǒng)中一個類只有一個實例而且該實例又易于外界訪問,從而方便對實例個數(shù)的控制并節(jié)約系統(tǒng)資源而出現(xiàn)的解決方案。如下圖:

單例模式簡單示意圖

餓漢式單例模式

什么叫做餓漢式單例?為了方便記憶,老貓是這么理解的,餓漢給人的形象就是有食物就迫不及待地去吃的形象。那么餓漢式單例模式的形象也就是當(dāng)類創(chuàng)建的時候就迫不及待地去創(chuàng)建單例對象,這種單例模式是絕對線程安全的,因為這種模式在尚未產(chǎn)生線程之前就已經(jīng)創(chuàng)建了單例。

看一下示例,如下:

/**
 * 公眾號:程序員老貓
 * 餓漢單例模式 
 */
public class HungrySingleton {

    private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();

    //構(gòu)造函數(shù)私有化,保證不被new方式多次創(chuàng)建新對象
    private HungrySingleton() {
    }

    public static HungrySingleton getInstance(){
        return HUNGRY_SINGLETON;
    }
}

我們看一下上述案例的優(yōu)缺點:

  • 優(yōu)點:線程安全,類加載時完成初始化,獲取對象的速度較快。
  • 缺點:由于類加載的時候就完成了對象的創(chuàng)建,有的時候我們無需調(diào)用的情況下,對象已經(jīng)存在,這樣的話就會造成內(nèi)存浪費。

當(dāng)前硬件和服務(wù)器的發(fā)展,快于軟件的發(fā)展,另外的,微服務(wù)和集群化部署,大大降低了橫向擴展的門檻和成本,所以老貓覺得當(dāng)前的內(nèi)存其實是不值錢的,所以上述這種單例模式硬說其缺點有多嚴重其實也不然,個人覺得這種模式用于實際開發(fā)過程中其實是沒有問題的。

其實在我們?nèi)粘J褂玫膕pring框架中,IOC容器本身就是一個餓漢式單例模式,spring啟動的時候就將對象加載到了內(nèi)存中,這里咱們不做展開,等到后續(xù)咱們梳理到spring源代碼的時候再展開來說。

懶漢式單例模式

上述餓漢單例模式我們說它的缺點是浪費內(nèi)存,因為其在類加載的時候就創(chuàng)建了對象,那么針對這種內(nèi)存浪費的解決方案,我們就有了“懶漢模式”。對于這種類型的單例模式,老貓是這么理解的,懶漢的定義給人的直觀感覺是懶惰、拖延。那么對應(yīng)的模式上來說,這種方案創(chuàng)建對象的方法也是在程序使用對象前,先判斷該對象是否已經(jīng)實例化(判空),若已實例化直接返回該類對象,否則則先執(zhí)行實例化操作。

看一下示例,如下:

/**
 * 公眾號:程序員老貓
 * 懶漢式單例模式
 */
public class LazySingleton {
    private LazySingleton() {
    }

    private static LazySingleton lazySingleton = null;
    public static LazySingleton getInstance() {
        if (lazySingleton == null) {
              lazySingleton =  new LazySingleton();
        }
        return lazySingleton;
    }
}

上面這種單例模式創(chuàng)建對象,內(nèi)存問題看起來是已經(jīng)解決了,但是這種創(chuàng)建方式真的就線程安全了么?咱們接下來寫個簡單的測試demo:

public class Main {
    public static void main(String[] args) {
        Thread thread1 = new Thread(()->{
            LazySingleton lazySingleton = LazySingleton.getInstance();
            System.out.println(lazySingleton.toString());
        });
        Thread thread2 = new Thread(()->{
            LazySingleton lazySingleton = LazySingleton.getInstance();
            System.out.println(lazySingleton.toString());
        });
        thread1.start();
        thread2.start();
        System.out.println("end");
    }
}

執(zhí)行輸出結(jié)果如下:

end
LazySingleton@3fde6a42
LazySingleton@2648fc3a

從上述的輸出中我們很容易地發(fā)現(xiàn),兩個線程中所獲取的對象是不同的,當(dāng)然這個是有一定概率性質(zhì)的。所以在這種多線程請求的場景下,就出現(xiàn)了線程安全性問題。

聊到共享變量訪問線程安全性的問題,我們往往就想到了鎖,所以,咱們在原有的代碼塊上加上鎖對其優(yōu)化試試,我們首先想到的是給方法代碼塊加上鎖。

加鎖后代碼如下:

public class LazySingleton {

    private LazySingleton() {
    }

    private static LazySingleton lazySingleton = null;
    public synchronized static LazySingleton getInstance() {
        if (lazySingleton == null) {
              lazySingleton =  new LazySingleton();
        }
        return lazySingleton;
    }
}

經(jīng)過上述同樣的測試類運行之后,我們發(fā)現(xiàn)問題似乎解決了,每次運行之后得到的結(jié)果,兩個線程對象的輸出都是一致的。

我們用線程debug的方式看一下具體的運行情況,如下圖:

線程輸出

我們可以發(fā)現(xiàn),當(dāng)一個線程進行初始化實例時,另一個線程就會從Running狀態(tài)自動變成了Monitor狀態(tài)。試想一下,如果有大量的線程同時訪問的時候,在這樣一個鎖的爭奪過程中就會有很多的線程被掛起為Monitor狀態(tài)。CPU壓力隨著線程數(shù)的增加而持續(xù)增加,顯然這種實現(xiàn)對性能還是很有影響的。

那還有優(yōu)化的空間么?當(dāng)然有,那就是大家經(jīng)常聽到的“DCL”即“Double Check Lock” 實現(xiàn)如下:

/**
 * 公眾號:程序員老貓
 * 懶漢式單例模式(DCL)
 * Double Check Lock
 */
public class LazySingleton {

    private LazySingleton() {
    }
    //使用volatile防止指令重排
    private volatile static LazySingleton lazySingleton = null;
    public static LazySingleton getInstance() {
        if (lazySingleton == null) {
            synchronized (LazySingleton.class) {
                if(lazySingleton == null){
                    lazySingleton =  new LazySingleton();
                }
            }
        }
        return lazySingleton;
    }
}

通過DEBUG,我們來看一下下圖:

雙重校驗鎖

這里引申一個常見的問題,大家在面試的時候估計也會碰到。問題:為什么要double check?去掉第二次check行不行?

回答:當(dāng)2個線程同時執(zhí)行g(shù)etInstance方法時,都會執(zhí)行第一個if判斷,由于鎖機制的存在,會有一個線程先進入同步語句,而另一個線程等待,當(dāng)?shù)谝粋€線程執(zhí)行了new Singleton()之后,就會退出synchronized的保護區(qū)域,這時如果沒有第二重if判斷,那么第二個線程也會創(chuàng)建一個實例,這就破壞了單例。

問題:這里為什么要加上volatile修飾關(guān)鍵字?回答:這里加上該關(guān)鍵字主要是為了防止"指令重排"。關(guān)于“指令重排”具體產(chǎn)生的原因我們這里不做細究,有興趣的小伙伴可以自己去研究一下,我們這里只是去分析一下,“指令重排”所帶來的影響。

lazySingleton =  new LazySingleton();

這樣一個看似簡單的動作,其實從JVM層來看并不是一個原子性的行為,這里其實發(fā)生了三件事:

  • 給LazySingleton分配內(nèi)存空間。
  • 調(diào)用LazySingleton的構(gòu)造函數(shù),初始化成員字段。
  • 將LazySingleton指向分配的內(nèi)存空間(注意此時的LazySingleton就不是null了)

在此期間存在著指令重排序的優(yōu)化,第2、3步的順序是不能保證的,最后的執(zhí)行順序可能是1-2-3,也可能是1-3-2,假如執(zhí)行順序是1-3-2,我們看看會出現(xiàn)什么問題??匆幌孪聢D:

指令重排執(zhí)行

從上圖中我們看到雖然LazySingleton不是null,但是指向的空間并沒有初始化,最終被業(yè)務(wù)使用的時候還是會報錯,這就是DCL失效的問題,這種問題難以跟蹤難以重現(xiàn)可能會隱藏很久。

JDK1.5之前JMM(Java Memory Model,即Java內(nèi)存模型)中的Cache、寄存器到主存的回寫規(guī)定,上面第二第三的順序無法保證。JDK1.5之后,SUN官方調(diào)整了JVM,具體化了volatile關(guān)鍵字,private volatile static LazySingleton lazySingleton;只要加上volatile,就可以保證每次從主存中讀?。ㄟ@涉及到CPU緩存一致性問題,感興趣的小伙伴可以研究研究),也可以防止指令重排序的發(fā)生,避免拿到未完成初始化的對象。

上面這種方式可以有效降低鎖的競爭,鎖不會將整個方法全部鎖定,而是鎖定了某個代碼塊。其實完全做完調(diào)試之后我們還是會發(fā)現(xiàn)鎖爭奪的問題并沒有完全解決,用到了鎖肯定會對整個代碼的執(zhí)行效率帶來一定的影響。所以是否存在保證線程的安全,并且能夠不浪費內(nèi)存完美的解決方案呢?一起看下下面的解決方案。

內(nèi)部靜態(tài)類單例模式

這種方式其實是利用了靜態(tài)對象創(chuàng)建的特性來解決上述內(nèi)存浪費以及線程不安全的問題。在這里我們要弄清楚,被static修飾的屬性,類加載的時候,基本屬性就已經(jīng)加載完畢,但是靜態(tài)方法卻不會加載的時候自動執(zhí)行,而是等到被調(diào)用之后才會執(zhí)行。并且被STATIC修飾的變量JVM只為靜態(tài)分配一次內(nèi)存。(這里老貓不展開去聊static相關(guān)知識點,有興趣的小伙伴也可以自行去了解一下更多JAVA中static關(guān)鍵字修飾之后的類、屬性、方法的加載機制以及存儲機制)

所以綜合這一特性,我們就有了下面這樣的寫法:

public class LazyInnerClassSingleton {
    private LazyInnerClassSingleton () {
    }

    public static final LazyInnerClassSingleton getInstance() {
        return LazyHolder.LAZY;
    }

    private static class LazyHolder {
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

上面這種寫法,其實也屬于“懶漢式單例模式”,并且這種模式相對于“無腦加鎖”以及“DCL”以及“餓漢式單例模式”來說無疑是最優(yōu)的一種實現(xiàn)方式。

但是深度去追究的話,其實這種方式也會有問題,這種寫法并不能防止反序列化和反射生成多個實例。我們簡單看一下反射的破壞的測試類:

public class DestructionSingletonTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class<LazyInnerClassSingleton> enumSingletonClass = LazyInnerClassSingleton.class;
        //枚舉默認有個String 和 int 類型的構(gòu)造器
        Constructor constructor = enumSingletonClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        //利用反射調(diào)用構(gòu)造方法兩次直接創(chuàng)建兩個對象,直接破壞單例模式
        LazyInnerClassSingleton singleton1 = (LazyInnerClassSingleton) constructor.newInstance();
        LazyInnerClassSingleton singleton2 = (LazyInnerClassSingleton) constructor.newInstance();
    }
}

這里序列化反序列化單例模式破壞老貓偷個懶,因為下面會有寫到,有興趣的小伙伴繼續(xù)看下文,老貓覺得這種破壞場景在真實的業(yè)務(wù)使用場景比較極端,如果不涉及底層框架變動,光從業(yè)務(wù)角度來看,上面這些單例模式的實現(xiàn)已經(jīng)管夠了。當(dāng)然如果硬是要防止上面的反射創(chuàng)建單例兩次問題也能解決,如下:

public class LazyInnerClassSingleton {
    private LazyInnerClassSingleton () {
        if(LazyHolder.LAZY != null) {
            throw new RuntimeException("不允許創(chuàng)建多個實例");
        }
    }

    public static final LazyInnerClassSingleton getInstance() {
        return LazyHolder.LAZY;
    }

    private static class LazyHolder {
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

寫到這里,可能大家都很疑惑了,咋還沒提及用單例模式優(yōu)化線程池創(chuàng)建。下面這不來了么,老貓個人覺得上面的這種方式進行創(chuàng)建單例還是比較好的,所以就用這種方式重構(gòu)一下線程池的創(chuàng)建,具體代碼如下:

public class InnerClassLazyThreadPoolHelper {
    public static void execute(Runnable runnable) {
        ThreadPoolExecutor threadPoolExecutor = ThreadPoolHelperHolder.THREAD_POOL_EXECUTOR;
        threadPoolExecutor.execute(runnable);
    }
    /**
     * 靜態(tài)內(nèi)部類創(chuàng)建實例(單例).
     * 優(yōu)點:被調(diào)用時才會創(chuàng)建一次實例
     */
    public static class ThreadPoolHelperHolder {
        private static final int CPU = Runtime.getRuntime().availableProcessors();
        private static final int CORE_POOL_SIZE = CPU + 1;
        private static final int MAXIMUM_POOL_SIZE = CPU * 2 + 1;
        private static final long KEEP_ALIVE_TIME = 1L;
        private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS;
        private static final int MAX_QUEUE_NUM = 1024;

        private ThreadPoolHelperHolder() {
        }

        private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, TIME_UNIT,
                new LinkedBlockingQueue<>(MAX_QUEUE_NUM),
                new ThreadPoolExecutor.AbortPolicy());
    }
}

到此就結(jié)束了嗎?當(dāng)然不是,我們之前說上面這種單例創(chuàng)建模式的弊端是可以被反射或者序列化給攻克,雖然這種還是比較少的,但是技術(shù)么,還是稍微鉆一下牛角尖。有沒有一種單例模式不懼反射以及單例模式呢?顯然是有的。我們看下被很多人認為完美單例模式的枚舉類的寫法。

枚舉式單例模式

public enum EnumSingleton {
    INSTANCE;
    private Object object;

    public Object getObject() {
        return object;
    }

    public void setObject(Object object) {
        this.object = object;
    }

    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

上面我們寫過反射模式破壞“靜態(tài)內(nèi)部類單例模式”,那么這里咱們補一下序列化反序列化的例子。具體如下:

public class EnumSingletonTest {
    public static void main(String[] args) {
        try {
            EnumSingleton instance2 = EnumSingleton.getInstance();
            instance2.setObject(new Object());

            FileOutputStream fileOutputStream = new FileOutputStream("EnumSingletonTest");
            ObjectOutputStream oos = new ObjectOutputStream(fileOutputStream);
            oos.writeObject(instance2);
            oos.flush();
            oos.close();

            FileInputStream fileInputStream = new FileInputStream("EnumSingletonTest");
            ObjectInputStream ois = new ObjectInputStream(fileInputStream);
            EnumSingleton instance1  = (EnumSingleton) ois.readObject();
            ois.close();
            System.out.println(instance2.getObject());
            System.out.println(instance1.getObject());
        }catch (Exception e) {
        }
    }
}

最終我們發(fā)現(xiàn)其輸出的結(jié)果是一致的。大家可以參考老貓的代碼自己寫一下測試,關(guān)于反射破壞的方式老貓就不展開了,因為上面已經(jīng)有寫法了,大家可以參考一下,自行做一下測試。

那么既然枚舉類的單例模式這么完美,我們就拿它來重構(gòu)線程池的獲取吧。具體代碼如下:

public enum EnumThreadPoolHelper {
    INSTANCE;

    private static final ThreadPoolExecutor executor;

    static {
        final int CPU = Runtime.getRuntime().availableProcessors();
        final int CORE_POOL_SIZE = CPU + 1;
        final int MAXIMUM_POOL_SIZE = CPU * 2 + 1;
        final long KEEP_ALIVE_TIME = 1L;
        final TimeUnit TIME_UNIT = TimeUnit.SECONDS;
        final int MAX_QUEUE_NUM = 1024;
        executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, TIME_UNIT,
                new LinkedBlockingQueue<>(MAX_QUEUE_NUM),
                new ThreadPoolExecutor.AbortPolicy());
    }

    public void execute(Runnable runnable) {
        executor.execute(runnable);
    }

}

當(dāng)然在上述中,針對賦值的方式老貓用了static代碼塊自動類加載的時候就創(chuàng)建好了對象,大家也可以做一下其他優(yōu)化。不過還是得要保證單例模式。判斷是否為單例模式,老貓這里有個比較粗糙的辦法。我們打印出成員對象變量的值,通過多次調(diào)用看看其值是否一樣即可。當(dāng)然如果大家還有其他好辦法也歡迎留言。

總結(jié)

針對單例模式相信大家對其有了一個不錯的認識了。在日常開發(fā)的過程中,其實我們都接觸過,spring框架中,IOC容器本身就是單例模式的,當(dāng)然上述老貓也有提及到??蚣苤械膯卫J剑蹅兊热渴崂硗戤呍O(shè)計模式之后再去做深入探討。

關(guān)于單例模式的優(yōu)點也是顯而易見的:

  • 提供了對唯一實例的受控訪問。
  • 因為在系統(tǒng)內(nèi)存中只存在一個對象,所以能夠節(jié)約系統(tǒng)資源,對于一些需要頻繁建立和銷毀的對象單例模式無疑能夠提升系統(tǒng)的性能。

那么缺點呢?大家有想過么?我們就拿上面的線程池創(chuàng)建這個例子來說事兒。我們整個業(yè)務(wù)系統(tǒng)其實有很多類別的線程池,如果說我們根據(jù)不同的業(yè)務(wù)類型去做線程池創(chuàng)建的拆分的話,咱們是不是需要寫很多個這樣的單例模式。那么對于實際的開發(fā)過程中肯定是不友好的。所以主要缺點可想而知。

  • 因為單例模式中沒有抽象層,所以單例類的擴展有很大的困難。
  • 從開發(fā)者角度來說,使用單例對象(尤其在類庫中定義的對象)時,開發(fā)人員必須記住自己不能使用new關(guān)鍵字實例化對象。

所以具體場景還得具體分析,上面的一些單例模式實現(xiàn),如果大家還有比較好的方式歡迎大家留言。

上面老貓聊到了不同業(yè)務(wù)調(diào)用創(chuàng)建不同業(yè)務(wù)線程池的問題,可能需要定義不同的threadFactory名稱,那么此時,我們該如何去做?帶著疑問,讓我們期待接下來的其他模式吧。

責(zé)任編輯:趙寧寧 來源: 程序員老貓
相關(guān)推薦

2021-09-07 10:44:35

異步單例模式

2025-06-26 00:40:13

2024-02-04 12:04:17

2021-03-02 08:50:31

設(shè)計單例模式

2021-02-01 10:01:58

設(shè)計模式 Java單例模式

2022-09-29 08:39:37

架構(gòu)

2021-04-15 09:18:22

單例餓漢式枚舉

2021-05-29 10:22:49

單例模式版本

2016-03-28 10:23:11

Android設(shè)計單例

2013-11-26 16:20:26

Android設(shè)計模式

2021-06-10 09:00:33

單例模式數(shù)據(jù)庫

2015-10-08 14:26:46

2021-02-07 23:58:10

單例模式對象

2011-03-16 10:13:31

java單例模式

2022-06-07 08:55:04

Golang單例模式語言

2022-02-06 22:30:36

前端設(shè)計模式

2024-03-06 08:09:47

單例模式軟件

2024-03-06 13:19:19

工廠模式Python函數(shù)

2015-09-06 11:07:52

C++設(shè)計模式單例模式

2016-10-09 09:37:49

javascript單例模式
點贊
收藏

51CTO技術(shù)棧公眾號