自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

一個(gè)單例模式,沒必要這么卷吧

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

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

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

故事

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

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

寫在前面

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

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

概要

單例模式定義

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

單例模式簡單示意圖

餓漢式單例模式

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

看一下示例,如下:

/**
 * 公眾號(hào):程序員老貓
 * 餓漢單例模式 
 */
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)缺點(diǎn):

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

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

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

懶漢式單例模式

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

看一下示例,如下:

/**
 * 公眾號(hào):程序員老貓
 * 懶漢式單例模式
 */
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)建方式真的就線程安全了么?咱們接下來寫個(gè)簡單的測試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),兩個(gè)線程中所獲取的對象是不同的,當(dāng)然這個(gè)是有一定概率性質(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)過上述同樣的測試類運(yùn)行之后,我們發(fā)現(xiàn)問題似乎解決了,每次運(yùn)行之后得到的結(jié)果,兩個(gè)線程對象的輸出都是一致的。

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

線程輸出

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

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

/**
 * 公眾號(hào):程序員老貓
 * 懶漢式單例模式(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,我們來看一下下圖:

雙重校驗(yàn)鎖

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

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

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

lazySingleton =  new LazySingleton();

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

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

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

指令重排執(zhí)行

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

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ā)生,避免拿到未完成初始化的對象。

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

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

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

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

public class LazyInnerClassSingleton {
    private LazyInnerClassSingleton () {
    }

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

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

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

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

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

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

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

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

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

寫到這里,可能大家都很疑惑了,咋還沒提及用單例模式優(yōu)化線程池創(chuàng)建。下面這不來了么,老貓個(gè)人覺得上面的這種方式進(jìn)行創(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)建實(shí)例(單例).
     * 優(yōu)點(diǎn):被調(diào)用時(shí)才會(huì)創(chuàng)建一次實(shí)例
     */
    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ù)么,還是稍微鉆一下牛角尖。有沒有一種單例模式不懼反射以及單例模式呢?顯然是有的。我們看下被很多人認(rèn)為完美單例模式的枚舉類的寫法。

枚舉式單例模式

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)部類單例模式”,那么這里咱們補(bǔ)一下序列化反序列化的例子。具體如下:

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)于反射破壞的方式老貓就不展開了,因?yà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代碼塊自動(dòng)類加載的時(shí)候就創(chuàng)建好了對象,大家也可以做一下其他優(yōu)化。不過還是得要保證單例模式。判斷是否為單例模式,老貓這里有個(gè)比較粗糙的辦法。我們打印出成員對象變量的值,通過多次調(diào)用看看其值是否一樣即可。當(dāng)然如果大家還有其他好辦法也歡迎留言。

總結(jié)

針對單例模式相信大家對其有了一個(gè)不錯(cuò)的認(rèn)識(shí)了。在日常開發(fā)的過程中,其實(shí)我們都接觸過,spring框架中,IOC容器本身就是單例模式的,當(dāng)然上述老貓也有提及到。框架中的單例模式,咱們等全部梳理完畢設(shè)計(jì)模式之后再去做深入探討。

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

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

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

  • 因?yàn)閱卫J街袥]有抽象層,所以單例類的擴(kuò)展有很大的困難。
  • 從開發(fā)者角度來說,使用單例對象(尤其在類庫中定義的對象)時(shí),開發(fā)人員必須記住自己不能使用new關(guān)鍵字實(shí)例化對象。

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

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

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

2021-09-07 10:44:35

異步單例模式

2024-02-04 12:04:17

2021-03-02 08:50:31

設(shè)計(jì)單例模式

2021-02-01 10:01:58

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

2022-09-29 08:39:37

架構(gòu)

2021-04-15 09:18:22

單例餓漢式枚舉

2021-05-29 10:22:49

單例模式版本

2013-11-26 16:20:26

Android設(shè)計(jì)模式

2016-03-28 10:23:11

Android設(shè)計(jì)單例

2021-06-10 09:00:33

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

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è)計(jì)模式

2015-10-08 14:26:46

2024-03-06 08:09:47

單例模式軟件

2024-03-06 13:19:19

工廠模式Python函數(shù)

2015-09-06 11:07:52

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

2016-10-09 09:37:49

javascript單例模式

2023-11-21 21:39:38

單例模式音頻管理器
點(diǎn)贊
收藏

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