Java創建型設計模式 —— 單例模式有這麼多種寫法你都知道嗎?

一、引言

還記得老師當初給我們講單例模式嗎? 小編還清楚記得老師講了一個是餓漢式一個是懶漢式,也講了兩者的實現方式。

那個時候不理解設計模式是做什麼的,就死記硬背記住了,應付一下面試什麼的。

如果你只知道兩種寫法看完文本肯定會有所收穫,如果你是大牛,那就可以點點贊什麼的哈哈哈哈哈

單例模式使用場景:

如果系統中有比較重量級的對象,並且只需要實例化一個的時候,就考慮使用單例模式。舉個實際例子,在實際業務中難免會把數據存儲到Elasticsearch(搜索引擎),那麼對於操作Elasticsearch來說,只需要實例化一個對象即可,這個對象負責與Elasticsearch進行數據交互,看下實際代碼如下:

一個很簡單的餓漢式單例模式,把實例化的過程寫在靜態塊當中,根據不同的啓動環境連接不同的地址的Elasticsearch,最後創建對象賦值給esOperateService。

/**
 * @Auther: IT 賤男
 * @Date: 2018/11/1 16:15
 * @Description: Elasticsearch 代理對象,使用單例模式餓漢式 - 靜態塊實現
 */
public class EsServiceProxy {

    private static EsOperateService esOperateService;

    // 私有構造,防止外部new
    private EsServiceProxy() {
    }

    // 提供獲取實例的靜態方法
    public static EsOperateService getEsOperateService() {
        return esOperateService;
    }

    static {
        String env = Foundation.server().getEnvType();
        if (StringUtils.isEmpty(env)) {
            throw new RuntimeException("環境變量env未配置,請檢查配置!");
        }

        env = env.toLowerCase();
        String baseurl = "http://192.168.188.21:8080/es/esOperateService";
        if (!env.equals("dev") && !env.equals("fat")) {
            if (env.equals("uat")) {
                baseurl = "http://192.168.188.22:8081/es/esOperateService";
            } else if (env.equals("pro")) {
                baseurl = "http://192.168.13.10:9082/es/esOperateService";
            }
        }

        // 通過代理工廠創建對象並且賦值給常量
        HessianProxyFactory hessianProxyFactory = new HessianProxyFactory();
        hessianProxyFactory.setOverloadEnabled(true);
        try {
            esOperateService = (EsOperateService) hessianProxyFactory.create(EsOperateService.class, baseurl);
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
    }
}

 

二、單例模式餓漢式 - 靜態常量

 餓漢式在對象實例化的時候,就會創建好對象,沒有達到懶加載的效果(懶加載:就是說這個對象我不要一開始就創建,等到我需要用的時候在創建),但是這樣就不會因爲多線程的問題導致創建多個實例。

如果保證這個對象,在系統中一定會有用到,那麼這種方式也是推薦使用的。

/**
 * @Auther: IT賤男
 * @Date: 2019/7/25 15:22
 * @Description: 單例模式 餓漢式 - 靜態常量
 *
 * 優點:寫法比較簡單,在類裝載的時候就完成實例化,避免了多線程的問題
 * 缺點:這種方式沒有達到懶加載的效果,可能會造成內存浪費 (如果系統中一定會用到這個對象,則就避免了內存浪費)
 *
 */
public class SingletonCase1 {

    // 私有構造方法,避免外部new
    private SingletonCase1() {
    }

    // 創建靜態常量
    private static final SingletonCase1 singleton = new SingletonCase1();

    // 給外部提供實例獲取方法
    public static SingletonCase1 getSingleton() {
        return singleton;
    }

}

三、單例模式餓漢式 - 靜態代碼塊

 如果保證這個對象,在系統中一定會有用到,那麼這種方式也是推薦使用的。

/**
 * @Auther: IT賤男
 * @Date: 2019/7/25 15:22
 * @Description: 單例模式 餓漢式 - 靜態代碼
 *
 * 優缺點和餓漢式靜態常量一致
 *
 */
public class SingletonCase2 {

    // 私有構造方法,避免外部new
    private SingletonCase2() {
    }

    // 創建靜態常量
    private static SingletonCase2 singleton;

    static {
        // 將對象的實例化放在靜態塊當中,則可以編寫一些邏輯代碼,如文章一開始舉的實戰例子
        singleton = new SingletonCase2();
    }

    // 給外部提供實例獲取方法
    public static SingletonCase2 getSingleton() {
        return singleton;
    }

}

四、單例模式懶漢式 - 線程不安全寫法

懶漢式:可以這樣去記憶理解,既然是懶漢式就是在類加載的時候,懶得去創建對象,等到要用的時候在創建,這樣也就實現了懶加載的效果。 但是這樣實現也會存在一個很嚴重的問題,那就是在多線程的情況下,會存在創建多個實例的現象,具體看如下實現代碼。

這種方式不推薦使用

/**
 * @Auther: IT賤男
 * @Date: 2019/7/25 15:22
 * @Description: 單例模式 懶漢式 - 線程不安全
 *
 * 優點:可以實現懶加載的效果
 * 缺點:在多線程的情況,可能會創建多個實例的情況
 *
 */
public class SingletonCase3 {

    // 私有構造方法,避免外部new
    private SingletonCase3() {
    }

    // 創建靜態常量
    private static SingletonCase3 singleton;

    // 給外部提供實例獲取方法
    public static SingletonCase3 getSingleton() {
        // 這裏會存在多線程的問題,假設線程一正在執行new SingletonCase3()的操作,此時的singleton還是爲null
        // 線程二進行 singleton == null 判斷,這個時候等式還是成立的,所以線程二也會執行創建對象的操作。
        if (singleton == null) {
            singleton = new SingletonCase3();
        }
        return singleton;
    }

}

五、單例模式懶漢式 - 線程安全、同步方法

這個是針對上面的懶漢式進行了改進,給方法加上了synchronized關鍵字,能給有效的解決多線程的問題,但是會影響執行效率,因爲所有的線程都要排隊執行,所以這種方式也不推薦使用

/**
 * @Auther: IT賤男
 * @Date: 2019/7/25 15:22
 * @Description: 單例模式 懶漢式 - 線程不安全
 *
 * 優點:可以解決多線程的問題
 * 缺點:執行效率太慢,因爲加上synchrionzed所有線程將會排隊等待
 *
 */
public class SingletonCase4 {

    // 私有構造方法,避免外部new
    private SingletonCase4() {
    }

    // 創建靜態常量
    private static SingletonCase4 singleton;

    // 給外部提供實例獲取方法
    // 這裏給方法上加了synchronized關鍵字,能夠保證只有一個線程執行,其他線程排隊等待
    public static synchronized SingletonCase4 getSingleton() {
        if (singleton == null) {
            singleton = new SingletonCase4();
        }
        return singleton;
    }

}

六、單例模式 - 雙重檢查

經過分析懶漢式,一共存在兩個問題:1 多線程會創建多個、2、執行效率的問題

針對以上兩個問題,所以就有了雙重檢查,這種方式不僅僅避免多線程的問題,還不會影響效率,也實現了懶加載,在公司中也比較常用。

這種方式推薦使用

/**
 * @Auther: IT賤男
 * @Date: 2019/7/25 15:22
 * @Description: 單例模式 雙重檢查
 *
 * 優點:推薦使用,能夠解決懶加載、多線程的問題
 *
 * volatile :
 * 1、保證此變量對所有線程的可見性,“可見性”指當一條線程修改了這個變量的值,新的值對與其他線程來說是立即得知的。
 * 2、禁止指令重排序優化。
 *
 */
public class SingletonCase5 {

    // 私有構造方法,避免外部new
    private SingletonCase5() {
    }

     /**
      *
     *  加了volatile關鍵字,能夠解決指令重排的問題,這裏簡單的解釋一下指令重排的問題,需要一點點java內存模型的基礎
     *
     *  執行這句代碼時 singleton = new SingletonCase5();
     *
     *  正常情況下是這樣的指令順序
     *  1、memory = allocate() 分配對象的內存空間
     *  2、ctorInstance() 初始化對象
     *  3、instance = memory 設置instance指向剛分配的內存
     *
     *  在多線程的情況下,JVM 和 CPU優化會發生指令重排,變成這樣了
     *  1、memory = allocate() 分配對象的內存空間
     *  3、instance = memory 設置instance指向剛分配的內存
     *  2、ctorInstance() 初始化對象
     *
     *  A線程在new SingletonCase5()的時候,如果在指令重排以後的情況下,執行到以上的步驟三時,這個時候對象還未初始化
     *  B線程進執行第一個if判斷的時候,則會直接返回對象,這個時候對象還是未初始化的,如果直接使用則會出現問題
     *
     */


    // 創建靜態常量
    // 這裏給常量加了volatile關鍵字,能夠保證此變量對所有線程對可見性
    // 當只要有一個線程修改了這個變量的值,那麼其他線程也就可以立馬獲取到改變之後的值。

    private static volatile SingletonCase5 singleton;

    // 給外部提供實例獲取方法
    public static SingletonCase5 getSingleton() {

        // 有些小夥伴在這裏有點疑問,這裏也加了synchronized關鍵字呀,爲什麼不會影響效率呢?
        // 小夥伴可以仔細看下代碼,假設線程一進來之後,通過了第一個if判斷,然後進入下面的代碼,並且鎖住了,然後創建了對象
        // 然後線程二進來,即使通過了第一個if判斷,等線程一執行完,此時的singleton已不再爲空,所以避免了創建多個對象
        // 那之後的線程如果再進來,直接在第一個if判斷就不通過了,不會有線程等待的現象,也不影響效率。
        if (singleton == null) {
            synchronized (SingletonCase5.class) {
                if (singleton == null) {
                    singleton = new SingletonCase5();
                }
            }
        }
        return singleton;
    }

}

七、單例模式 - 靜態內部類

這種方式也是可以推薦使用的,也避免了之前懶漢式的問題,編碼也比較簡單。

利用了jvm在裝載類的時候,線程是安全的,也利用了內部類的在加載類時,不會加載內部類,所以這樣寫也是一種方式。

/**
 * @Auther: IT賤男
 * @Date: 2019/7/25 15:22
 * @Description: 單例模式 懶漢式 - 靜態內部類
 *
 * 優點:
 * 1、靜態內部類在類加載的時候,是不會被加載的,實現了懶加載的特性
 * 2、在調用獲取實例的方法是,會去裝載內部類,在jvm裝載類的時候線程是安全的,靜態屬性只會在裝載類初始化一次
 *
 */
public class SingletonCase6 {

    // 私有構造方法,避免外部new
    private SingletonCase6() {
    }

    // 創建靜態內部類,提供常量
    private static class SingletonInstance {
        private static final SingletonCase6 SINGLETON = new SingletonCase6();
    }

    // 給外部提供實例獲取方法
    public static SingletonCase6 getSingleton() {
        return SingletonInstance.SINGLETON;
    }

}

八、單例模式 - 枚舉實現

枚舉的實現方式確實能夠達到單例模式所期待的效果,但小編在工作當中也沒有遇到過實際的使用場景。

這種實現方式也不存在有什麼問題,所以也是值得被推薦使用

/**
 * @Auther: IT賤男
 * @Date: 2019/7/25 15:22
 * @Description: 單例模式 懶漢式 - 枚舉實現
 * <p>
 * 優點:藉助JDK枚舉來實現單例模式,不僅能避免多線程同步的問題,而且還能防止反序列化重新創建新的對象
 */
public class SingletonCase8 {

    // 私有構造方法,避免外部new
    private SingletonCase8() {
    }

    // 給外部提供實例獲取方法
    public static SingletonCase8 getSingleton() {
        return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton {
        INSTANCE;

        private SingletonCase8 singletonCase8;
        
        // JVM 保證了這個方法絕對只調用一次
        Singleton() {
            singletonCase8 = new SingletonCase8();
        }

        public SingletonCase8 getInstance() {
            return singletonCase8;
        }

    }
}

九、總的來說

值得推薦使用的幾種方式有:

1、如果保證這個對象在項目中一定有使用到,那麼餓漢式是值得推薦使用的,在項目中比較常用。

2、雙重檢查方式推薦使用,在項目中比較常用。

3、靜態內部類、枚舉實現方式推薦使用

 

那麼這幾種應該是市面上所有的常見的單例模式實現的方式,小編對每一種方式進行分析,也說明了優缺點。小夥伴可以根據不同的業務場景來選擇不同的實現方式。

小編通過簡單文字說明和註釋來講解的單例模式的,如果有小夥伴哪裏有疑惑的可以評論留言,小編看到了會及時回覆的。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章