設(shè)計模式修煉之路-單例模式

單例模式應(yīng)該是我們平時聽過最多的設(shè)計模式,也是最簡單的設(shè)計模式,面試的時候也經(jīng)常會被問到,有的面試官還動不動讓你手寫一個單例模式。我們來一起了解一下單例模式吧。

一、什么是單例模式?

單例顧名思義就是單個的實例(對象)。單例模式屬于創(chuàng)建型模式,它只涉及到單個的類,不通過new的方式創(chuàng)建對象,而是由類本身提供的方法創(chuàng)建和訪問自己的對象,同時保證對象不會被多次創(chuàng)建,多次訪問該方法時訪問的是同一個對象。

二、單例模式的優(yōu)缺點

優(yōu)點:
1.有效的減少資源消耗。
2.方便對象狀態(tài)的共享。
3.避免對資源的多重占用(如寫文件操作時文件被多個對象占用)。
缺點:
1.因為狀態(tài)共享,所以不適合同一個類需要在不同場景保存各自狀態(tài)的情況。
2.不能繼承(登記式除外),因此無法通過子類擴展功能。
3.違反了單一職責(zé)原則。因為實例化自身的職責(zé)是由自身完成的。
4.對于保存狀態(tài)的單例對象,如果長時間未調(diào)用,對象有可能會被回收導(dǎo)致狀態(tài)丟失。

三、單例模式的應(yīng)用場景版本

單例模式的主要應(yīng)用場景如下:
1.對象需要頻繁創(chuàng)建、銷毀的時候,例如被spring管理的bean默認都是單例模式,如果不是單例模式我們每次http請求的時候就需要創(chuàng)建很多對象,請求結(jié)束又要銷毀這些對象。
2.創(chuàng)建對象需要時消耗時間較長或消耗資源較多且使用較頻繁的對象。例如一些從文件讀取數(shù)據(jù)的配置類,i/o操作就比較耗時耗資源。
3.需要全局共享狀態(tài)或者資源的類,例如線程池、數(shù)據(jù)庫連接池、保存了狀態(tài)的工具類等。

四、單例模式的實現(xiàn)方式

1.懶漢式

懶漢式,在系統(tǒng)啟動的時候不會實例化對象,只有在第一次獲取對象時才會創(chuàng)建對象.

1.1線程不安全

這種實現(xiàn)方式適合用于單線程場景,多線程場景會創(chuàng)建多個實例。

public class Singleton1 {

    private static Singleton1 instance;

    private Singleton1() {
        System.out.println("調(diào)用構(gòu)造函數(shù)創(chuàng)建對象");
    }

    public static Singleton1 getInstance() {
        if (instance == null) {
            instance = new Singleton1();
        }
        return instance;
    }
}

我們創(chuàng)建10個線程調(diào)用getInstance()方法進行測試,代碼如下:

public static void main(String[] args) {
        singleton1();
    }

private static void singleton1(){
        List<Thread> threads = new ArrayList<>(10);
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(Singleton1::getInstance));
        }
        threads.forEach(Thread::start);
    }

測試結(jié)果如下:

Task :Test.main()
調(diào)用構(gòu)造函數(shù)創(chuàng)建對象
調(diào)用構(gòu)造函數(shù)創(chuàng)建對象
調(diào)用構(gòu)造函數(shù)創(chuàng)建對象
調(diào)用構(gòu)造函數(shù)創(chuàng)建對象
調(diào)用構(gòu)造函數(shù)創(chuàng)建對象

可以看出構(gòu)造函數(shù)被調(diào)用了多次,也就是說對象被多次實例化,并未實現(xiàn)單例。

1.2線程安全

下述方式對getInstance()方法加了鎖,因此是線程安全的。加鎖會影響效率,適用于getInstance()方法的性能對程序不是很重要的場景。

public class Singleton2 {

    private static Singleton2 instance;

    private Singleton2() {
        System.out.println("調(diào)用構(gòu)造函數(shù)創(chuàng)建對象");
    }

    public static synchronized Singleton2 getInstance() {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }
}

我們創(chuàng)建10個線程調(diào)用getInstance()方法進行測試,代碼如下:

 public static void main(String[] args) {
        singleton2();
    }

    private static void singleton2(){
        List<Thread> threads = new ArrayList<>(10);
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(Singleton2::getInstance));
        }
        threads.forEach(Thread::start);
    }

反復(fù)執(zhí)行多次結(jié)果均如下:

Task :Test.main()
調(diào)用構(gòu)造函數(shù)創(chuàng)建對象
可以看出對象只被創(chuàng)建了一次,因此時線程安全的。

2.餓漢式

餓漢式是在類被加載的時候就創(chuàng)建好了對象,因此是線程安全的,并且沒有加鎖,對不會影響程序效率,但是因為是在未使用之前加載的,所以會浪費一些內(nèi)存。

public class Singleton3 {

    private static Singleton3 instance = new Singleton3();

    private Singleton3() {
        System.out.println("調(diào)用構(gòu)造函數(shù)創(chuàng)建對象");
    }

    public static Singleton3 getInstance() {
        return instance;
    }
}

3.雙重校驗鎖

這種方式采用雙鎖機制,只鎖了創(chuàng)建對象的一部分代碼,因此在多線程情況下依然能保持高性能。下面的例子,有的童鞋可能覺得和方法上加鎖性能差不多,如果在標記1和標記4處有一些耗時操作,那么第一個線程在創(chuàng)建對象的時候第二個線程已經(jīng)可以進入方法在標記1處執(zhí)行耗時操作了,等到第一個線程創(chuàng)建完對象兩個線程又可以同時執(zhí)行標記4處的耗時操作,如果是方法上加鎖第二個線程必須等到第一個線程執(zhí)行完標記1和標記4處的耗時操作才能再進入方法。
標記3處的第二個null判斷的意義又是什么呢?此處的null判斷非常關(guān)鍵,如果有兩個線程同時執(zhí)行到了標記2處,其中一個線程拿到了鎖,創(chuàng)建了對象,釋放鎖后另一個線程拿到鎖后如果沒有標記3處的null判斷,第二個線程就會再創(chuàng)建一個對象,就無法保證單例。

public class Singleton4 {

    private volatile static Singleton4 instance = new Singleton4();

    private Singleton4() {
        System.out.println("調(diào)用構(gòu)造函數(shù)創(chuàng)建對象");
    }

    public static Singleton4 getInstance() {
        //標記1
        if (instance == null){
            //標記2
            synchronized (Singleton4.class){
                //標記3
                if (instance==null){
                    instance = new Singleton4();
                }
            }
        }
        //標記4
        return instance;
    }
}

4.登記式/靜態(tài)內(nèi)部類

這種方式的功效跟雙重校驗鎖一樣,但實現(xiàn)方式更為簡單。利用了classloader加載類的原理,保證了初始化Singleton5時只有一個線程,同時保證了了初始化Singleton5時它的靜態(tài)內(nèi)部類SingletonInner不會被加載,這樣Singleton5對象(INSTANCE)也就不會被創(chuàng)建。

public class Singleton5 {

    public static class SingletonInner {
        public static final Singleton5 INSTANCE = new Singleton5();
    }

    private Singleton5() {
        System.out.println("調(diào)用構(gòu)造函數(shù)創(chuàng)建對象");
    }

    public static Singleton5 getInstance() {

        return SingletonInner.INSTANCE;
    }
}

5.枚舉

這種方式利用了枚舉類本身的特性,支持序列化機制可以在反序列化時避免重復(fù)創(chuàng)建對象,同時還能避免線程同步問題。但這種方式在實際使用中用的比較少。

public enum  Singleton6 {
    INSTANCE
    //下面可以寫一些方法
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容