單例模式你會幾種寫法?

前言

只有光頭才能變強html

回顧前面:java

原本打算沒那麼快更新的,這陣子在刷Spring的書籍。在看Spring的時候又常常會看到「單例」,「工廠」這些字樣。程序員

因此,就先來講說單例和工廠設計模式啦,這兩種模式也是很常見的,我看不少面經都會遇到這兩種模式~設計模式

本文主要講解單例設計模式,若是有錯的地方但願能多多包涵,並不吝在評論區指正!安全

1、單例模式概述

單例模式定義很簡單:一個類中能建立一個實例,因此稱之爲單例!微信

那咱們何時會用到單例模式呢??多線程

  • 那咱們想一想既然一個類中只能建立一個實例了,那麼能夠說這是跟類的狀態與對象無關的了。
  • 頻繁建立對象、管理對象是一件耗費資源的事,咱們只須要建立一個對象來用就足夠了!

學過Java Web的同窗可能就知道:函數

  • Servlet是單例的
  • Struts2是多例的
  • SpringMVC是單例的

那既然多例是頻繁建立對象、須要管理對象的,那Struts2爲何要多例呢??post

  • 主要因爲設計層面上的問題,Struts2是基於Filter攔截類的,ognl引擎對變量是注入的。因此它要設計成多例的~

能使用一個對象來作就不用實例化多個對象!這就能減小咱們空間和內存的開銷~性能

那有可能有的人又會想了:咱們使用靜態類.doSomething()和使用單例對象調用方法的效果是同樣的啊。

  • 沒錯,效果就是同樣的。使用靜態類.doSomething()體現的是基於對象,而使用單例設計模式體現的是面向對象

2、編寫單例模式的代碼

編寫單例模式的代碼其實很簡單,就分了三步:

  • 將構造函數私有化
  • 在類的內部建立實例
  • 提供獲取惟一實例的方法

2.1餓漢式

根據上面的步驟,咱們就能夠輕鬆完成建立單例對象了。

public class Java3y {

    // 1.將構造函數私有化,不能夠經過new的方式來建立對象
    private Java3y(){}

    // 2.在類的內部建立自行實例
    private static Java3y java3y = new Java3y();

    // 3.提供獲取惟一實例的方法
    public static Student getJava3y() {
        return java3y;
    }
}

這種代碼咱們稱之爲:「餓漢式」:

  • 一上來就建立對象了,若是該實例從始至終都沒被使用過,則會形成內存浪費

2.2簡單懶漢式

既然說一上來就建立對象,若是沒有用過會形成內存浪費:

  • 那麼咱們就設計用到的時候再建立對象
public class Java3y {

    // 1.將構造函數私有化,不能夠經過new的方式來建立對象
    private Java3y(){}

    // 2.1先不建立對象,等用到的時候再建立
    private static Java3y java3y = null;

    // 2.1調用到這個方法了,證實是要被用到的了
    public static Java3y getJava3y() {

        // 3. 若是這個對象引用爲null,咱們就建立並返回出去
        if (java3y == null) {
            java3y = new Java3y();
        }

        return java3y;
    }
}

上面的代碼行不行??在單線程環境下是行的,在多線程環境下就不行了

要解決也很簡單,咱們只要加鎖就行了:

 

 

2.3雙重檢測機制(DCL)懶漢式

上面那種直接在方法上加鎖的方式其實不夠好,由於在方法上加了內置鎖在多線程環境下性能會比較低下,因此咱們能夠將鎖的範圍縮小

public class Java3y {


    private Java3y() {
    }

    private static Java3y java3y = null;


    public static Java3y getJava3y() {
        if (java3y == null) {
            // 將鎖的範圍縮小,提升性能
            synchronized (Java3y.class) {
                java3y = new Java3y();
            }
        }
        return java3y;
    }
}

那上面的代碼可行嗎??不行,由於雖然加了鎖,但仍是有可能建立出兩個對象出來的:

  • 線程A和線程B同時調用getJava3y()方法,他們同時判斷java==null,得出的結果都是爲null,因此進入了if代碼塊了
  • 此時線程A獲得CPU的控制權-->進入同步代碼塊-->建立對象-->返回對象
  • 線程A完成了之後,此時線程B獲得了CPU的控制權。一樣是-->進入同步代碼塊-->建立對象-->返回對象
  • 很明顯的是:Java3y類返回了不止一個實例!因此上面的代碼是不行的!

有的同窗可能以爲我瞎吹比,明明加鎖了還不行?咱們來測試一下:

public class TestDemo {

    public static void main(String[] args) {

        // 線程A
        new Thread(() -> {

            // 建立單例對象
            Java3y java3y = Java3y.getJava3y();
            System.out.println(java3y);

        }).start();

        // 線程B
        new Thread(() -> {
            // 建立單例對象
            Java3y java3y = Java3y.getJava3y();
            System.out.println(java3y);
        }).start();

        // 線程C
        new Thread(() -> {
            // 建立單例對象
            Java3y java3y = Java3y.getJava3y();
            System.out.println(java3y);
        }).start();

    }
}

能夠看到,打印出的對象不僅僅只有一個的!

 

 

厲害的程序員又想到了:進入同步代碼塊時再判斷一下對象是否存在就穩了吧

  • 因此,有了下面的代碼
public class Java3y {


    private Java3y() {
    }

    private static Java3y java3y = null;

    public static Java3y getJava3y() {
        if (java3y == null) {

            // 將鎖的範圍縮小,提升性能
            synchronized (Java3y.class) {

                // 再判斷一次是否爲null
                if (java3y == null) {
                    java3y = new Java3y();
                }
            }
        }
        return java3y;
    }
}

其實還不穩!這裏會有重排序的問題

 

 

原本想測試重排序問題的效果的,一直沒測試出來~~~有相關測試代碼的但願能夠告訴我怎麼能測出來....

要解決也十分簡單,加上咱們的volatile關鍵字就能夠了,volatile有內存屏障的功能

具體可參考資料:

因此說,完整的DCL代碼是這樣子的:

public class Java3y {
    private Java3y() {
    }

    private static volatile Java3y java3y = null;

    public static Java3y getJava3y() {
        if (java3y == null) {

            // 將鎖的範圍縮小,提升性能
            synchronized (Java3y.class) {

                // 再判斷一次是否爲null
                if (java3y == null) {
                    java3y = new Java3y();
                }
            }
        }
        return java3y;
    }
}

再說明:

 

 

2.4靜態內部類懶漢式

還可使用靜態內部類這種巧妙的方式來實現單例模式!它的原理是這樣的:

  • 當任何一個線程第一次調用getInstance()時,都會使SingletonHolder被加載和被初始化,此時靜態初始化器將執行Singleton的初始化操做。(被調用時才進行初始化!)
  • 初始化靜態數據時,Java提供了的線程安全性保證。(因此不須要任何的同步)
public class Java3y {


    private Java3y() {
    }

    // 使用內部類的方式來實現懶加載
    private static class LazyHolder {
        // 建立單例對象
        private static final Java3y INSTANCE = new Java3y();
    }


    // 獲取對象
    public static final Java3y getInstance() {
        return LazyHolder.INSTANCE;
    }
    
}

靜態內部類這種方式是很是推薦使用的!不少人沒接觸過單例模式的都不知道有這種寫法,這種寫法很優化也高效!

參考資料:

2.5枚舉方式實現

使用枚舉就很是簡單了:

public enum Java3y3y {
    
    JAVA_3_Y_3_Y,
}

那這種有啥好處??枚舉的方式實現:

  • 簡單,直接寫就好了
  • 防止屢次實例化,即便是在面對複雜的序列化或者反射攻擊的時候(安全)!

這種也較爲推薦使用!

3、總結

總的來講單例模式寫法有5種:

  • 餓漢式
  • 簡單懶漢式(在方法加鎖)
  • DCL雙重檢測加鎖(進階懶漢式)
  • 靜態內部類實現懶漢式(最推薦寫法)
  • 枚舉方式(最安全、簡潔寫法)

明天估計寫的是工廠模式了,敬請期待哦~~~

參考資料:

若是文章有錯的地方歡迎指正,你們互相交流。習慣在微信看技術文章,想要獲取更多的Java資源的同窗,能夠關注微信公衆號:Java3y。爲了你們方便,剛新建了一下qq羣:742919422,你們也能夠去交流交流。謝謝支持了!但願能多介紹給其餘有須要的朋友

文章的目錄導航

相關文章
相關標籤/搜索