單例模式正確使用方式


這次分享我們就來談談單例模式的使用,其實在本公眾號設計模式的第一篇分享就是單例模式,為什么又要討論單例模式了?主要是那篇文章談的比較淺,只對單例模式的主要思想做了一個分享,這篇文章會從多個方面去分享單例模式的使用,下面進入正題。

使用Java做程序的小伙伴都知道單例,尤其是使用spring框架做項目的,大家都知道spring框架管理類默認都是單例模式的,並且是線程安全的。那么如果保證一個類只被初始化一次且線程安全了?帶着這個問題我們開始這篇文章。

在剛接觸設計模式的時候,大多數人首先接觸到的第一個設計模式就是單例模式,當然大多數人都只停留在單例模式,在平時的開發中用的少之又少,這篇文章會幫助大家從多個方面理解單例模式,此文代碼較多,如果對單例模式概念不是很清楚的小伙伴可以看另一篇文章《設計模式之單例模式》。

1. 懶漢模式

package com.study.concurrency.base_concurrency.example.singleton;

/**
 * <p>Title: Singleton01</p >
 * <p>Description: 懶漢模式  單例實例在第一次使用時進行創建 </p >
 * <p>Company: http://www.yinjiedu.com</p >
 * <p>Project: concurrency</p >
 *
 * @author: qiwei
 * @Date: 2019/8/20 22:41
 * @Version: 1.0
 */
public class Singleton01 {

    private Singleton01() {

    }

    //單例對象
    private static Singleton01 instance = null;

    /**
     * @description: 靜態的工廠方法
     * @auther: qiwei
     * @date: 2019/8/20 22:43
     * @return: Singleton01
     */
    public static Singleton01 getInstance() {
        if (instance == null) {
            instance = new Singleton01();
        }
        return instance;
    }
}

上述代碼在單線程下是沒有問題的,但是在多線程情況下是線程不安全的,出現問題的代碼主要是在下面這段代碼:

2. 餓漢模式

為了解決上面懶漢模式線程不安全問題,我們可以使用餓漢模式創建類實例。餓漢模式是單例實例在類裝載時進行創建,代碼如下:

package com.study.concurrency.base_concurrency.example.singleton;

/**
 * <p>Title: Singleton02</p >
 * <p>Description: 餓漢模式</p >
 * <p>Company: http://www.yinjiedu.com</p >
 * <p>Project: concurrency</p >
 *
 * @author: qiwei
 * @Date: 2019/8/20 22:53
 * @Version: 1.0
 */
public class Singleton02 {

    private Singleton02() {
    }

    //單例對象
    private static Singleton02 instance = new Singleton02();

    /**
     * @description: 靜態的工廠方法
     * @auther: qiwei
     * @date: 2019/8/20 22:55
     * @return: Singleton02
     */
    public static Singleton02 getInstance() {
        return instance;
    }
}

餓漢模式問題

餓漢模式雖然是線程安全的,但是在實際開發中,會有一些大家需要注意的問題。

第一點:餓漢模式是在類裝載的時候就創建的,所以如果類的構造方法里面很多復雜的處理,類裝載就會比較慢,影響程序性能;

第二點:餓漢模式創建的示例,不管在項目其他地方有沒有使用,類的實例已經創建,如果不使用,會造成資源的浪費。

基於以上兩點,大家在使用餓漢模式的時候需要注意實際的使用場景。

3. 懶漢模式之線程安全

我們看到在使用懶漢模式創建實例時是線程不安全的,這里我們使用同步鎖的方式,讓懶漢模式也是線程安全的,代碼如下:

package com.study.concurrency.base_concurrency.example.singleton;

/**
 * <p>Title: Singleton01</p >
 * <p>Description: 懶漢模式 </p >
 * <p>Company: http://www.yinjiedu.com</p >
 * <p>Project: concurrency</p >
 *
 * @author: qiwei
 * @Date: 2019/8/20 22:41
 * @Version: 1.0
 */
public class Singleton03 {

    private Singleton03() {

    }

    //單例對象
    private static Singleton03 instance = null;

    /**
     * @description: 靜態的工廠方法
     * @auther: qiwei
     * @date: 2019/8/20 22:43
     * @return: Singleton01
     */
    public static synchronized Singleton03 getInstance() {
        if (instance == null) {
            instance = new Singleton03();
        }
        return instance;
    }
}

這里我們使用了synchronized關鍵字保證了線程安全,但是在實際開發中不要這樣寫,因為synchronized會帶來比較嚴重的性能開銷。

4. 懶漢模式之雙重同步鎖模式(線程不安全)

使用雙重同步鎖實現單例模式,代碼如下:

package com.study.concurrency.base_concurrency.example.singleton;

/**
 * <p>Title: Singleton01</p >
 * <p>Description: 懶漢模式 </p >
 * <p>Company: http://www.yinjiedu.com</p >
 * <p>Project: concurrency</p >
 *
 * @author: qiwei
 * @Date: 2019/8/20 22:41
 * @Version: 1.0
 */
public class Singleton04 {

    private Singleton04() {

    }

    //單例對象
    private static Singleton04 instance = null;

    /**
     * @description: 靜態的工廠方法
     * @auther: qiwei
     * @date: 2019/8/20 22:43
     * @return: Singleton01
     */
    public static  Singleton04 getInstance() {
        if (instance == null) {
            synchronized (Singleton04.class) {
                if (instance == null) {
                    instance = new Singleton04();
                }
            }
        }
        return instance;
    }
}

上面代碼在synchronized這里啟用了雙重檢測機制,但是這種寫法在多線程情況下也是線程不安全的,這個原因是JVM和CPU在的指令重排;這里簡單的介紹一下什么是計算機指令:

Java實例化一個對象時,主要經歷下面三步指令:

第一步:分配對象內存空間

第二步:初始化對象

第三步:設置instance指向剛分配的內存

知道了上面步驟,我們知道一旦在多線程情況下發生了指令重排,就會出現安全問題,例如上面的步驟123變成132

5. 懶漢模式之雙重同步鎖模式(線程安全)

使用第四種方式創建單例也是線程不安全的,要想使第四個例子線程安全,我們需要使用到一個關鍵字:volatile。

代碼如下:

package com.study.concurrency.base_concurrency.example.singleton;

/**
 * <p>Title: Singleton01</p >
 * <p>Description: 懶漢模式 </p >
 * <p>Company: http://www.yinjiedu.com</p >
 * <p>Project: concurrency</p >
 *
 * @author: qiwei
 * @Date: 2019/8/20 22:41
 * @Version: 1.0
 */
public class Singleton05 {

    private Singleton05() {

    }

    //單例對象
    private volatile static Singleton05 instance = null;

    /**
     * @description: 靜態的工廠方法
     * @auther: qiwei
     * @date: 2019/8/20 22:43
     * @return: Singleton01
     */
    public static Singleton05 getInstance() {
        if (instance == null) {
            synchronized (Singleton05.class) {
                if (instance == null) {
                    instance = new Singleton05();
                }
            }
        }
        return instance;
    }
}

volatile可以禁止指令重排,所以上面方式是線程安全的。volatile使用場景主要是:狀態標示量、雙重檢測。

6. 餓漢模式之靜態域實現(錯誤寫法)

餓漢模式第一種實現使用了直接初始化方式,這里我們可以使用靜態域實現,這也是在平時開發中比較常見的一種方式,但是這里有一個坑,先埋給大家,代碼如下:

package com.study.concurrency.base_concurrency.example.singleton;

/**
 * <p>Title: Singleton02</p >
 * <p>Description: 餓漢模式</p >
 * <p>Company: http://www.yinjiedu.com</p >
 * <p>Project: concurrency</p >
 *
 * @author: qiwei
 * @Date: 2019/8/20 22:53
 * @Version: 1.0
 */
public class Singleton06 {

    private Singleton06() {

    }

    static {
        instance = new Singleton06();
    }
    //單例對象
    private static Singleton06 instance = null;

    /**
     * @description: 靜態的工廠方法
     * @auther: qiwei
     * @date: 2019/8/20 22:55
     * @return: Singleton02
     */
    public static Singleton06 getInstance() {
        return instance;
    }
}

上面靜態域單例模式是有問題的,大家想想有什么問題,在最后我們再討論,大家也可以debug調試找找問題。

7. 單例模式之枚舉創建

在前面集中創建單例的例子中,我們接觸了懶漢模式、餓漢模式。個人認為不管是懶漢模式還是餓漢模式,都不是最好的方案,這里我推薦使用枚舉類創建單例,既保證了線程安全的需求,又保證了資源不被浪費。代碼實現如下:

package com.study.concurrency.base_concurrency.example.singleton;

/**
 * <p>Title: Singleton01</p >
 * <p>Description: 枚舉模式 </p >
 * <p>Company: http://www.yinjiedu.com</p >
 * <p>Project: concurrency</p >
 *
 * @author: qiwei
 * @Date: 2019/8/20 22:41
 * @Version: 1.0
 */
public class Singleton07 {

    //私有構造函數
    private Singleton07() {

    }

    //單例對象
    private static Singleton07 instance = null;


    public static Singleton07 getInstance() {
        return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton {
        INSTANCE;

        private Singleton07 singleton;

        Singleton() {
            singleton = new Singleton07();
        }

        public Singleton07 getInstance() {
            return singleton;
        }
    }
}

上面代碼,在使用枚舉構造方法的時候JVM保證這個方法絕對只調用一次,個人推薦這種創建單例的模式。

解決6:餓漢模式之靜態域實現問題

第六種餓漢模式創建單例的方式,主要是因為代碼位置問題引起的,正確寫法是把下面兩段代碼交換位置:

private static Singleton06 instance = null;
static {
    instance = new Singleton06();
}

這是在使用靜態域時候特別容易犯的錯誤,希望大家注意。

關於單例模式的分享就到這里,不足之處希望大家提出來,我會及時修正。

  獲取實時信息,關注公眾號:『編程之藝術』,二維碼:

  

  

  

  

  

  

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM