設計模式 | 單例模式

寫在前面

單例對象(Singleton)是一種常用的設計模式。在Java應用中,單例對象能保證在一個JVM中,該對象只有一個實例存在。這樣的模式有幾個好處:

1、某些類創建比較頻繁,對於一些大型的對象,這是一筆很大的系統開銷。

2、省去了new操作符,降低了系統內存的使用頻率,減輕GC壓力。

3、有些類如交易所的核心交易引擎,控制着交易流程,如果該類可以創建多個的話,系統完全亂了。(比如一個軍隊出現了多個司令員同時指揮,肯定會亂成一團),所以只有使用單例模式,才能保證核心交易服務器獨立控制整個流程。

ACTION:

先來一個簡單的單利模式,(返回一個string對象)瞭解一下。

StrUtils.javva

public class StrUtils {
    private static String str = null;
    private StrUtils(){}
    //整個對象的 創建 加鎖 影響性能
    public static String getInstance(){
        if(str == null){
            System.out.println("getInstance");
            str = new String(UUID.randomUUID().toString());
        }
        return str;
    }

}

在單線程模式下,幾乎沒問題,放心使用。但是,在多線程環境下就會出現問題,返回的str並不是同一個對象。接下來測試一番。

測試類如下:

Testtt.java

public class Testtt {
    public static void main(String[] args) {

        for(int i=0; i<20; i++ ){
            new Thread(()->System.out.println(StrUtils.getInstance().hashCode())).start();
        }
    }

}

結果如下:

可以看出 返回的確是不是同一個String對象,而且Instance也執行了多次。 這是因爲 多線程同時到達 if(str == null)這裏面。

所以在多線程給出如下方案s:

方案一:

在 getinstance方法上面添加  synchronized 關鍵字。如下:

public static synchronized String getInstance(){
        if(str == null){
            System.out.println("getInstance");
            str = new String(UUID.randomUUID().toString());
        }
        return str;
}

可以是可以,但是性能堪憂啊。這樣豈不是每次進來都會堵塞,一個優秀的程序員是不允許這種情況的。

方案二:

由於只是初始化一次,所以只在 創建對象的時候進行阻塞即可。

public static  String getInstance(){
        if(str == null){
            // 只在第一次創建的時候加鎖,往後不會走這個,JVM離間就一個StrUtils,即str
            synchronized (StrUtils.class){
                System.out.println("getInstance");
                str = new String(UUID.randomUUID().toString());
            }

        }
        return str;
}

似乎解決了之前提到的問題,將synchronized關鍵字加在了內部,也就是說當調用的時候是不需要加鎖的,只有在instance爲null,並創建對象的時候才需要加鎖,性能有一定的提升。但是,這樣的情況,還是有可能有問題的,看下面的情況:在Java指令中創建對象和賦值操作是分開進行的,也就是說instance = new Singleton();語句是分兩步執行的。但是JVM並不保證這兩個操作的先後順序,也就是說有可能JVM會爲新的Singleton實例分配空間,然後直接賦值給instance成員,然後再去初始化這個Singleton實例。這樣就可能出錯了,我們以A、B兩個線程爲例:

a>A、B線程同時進入了第一個if判斷

b>A首先進入synchronized塊,由於instance爲null,所以它執行str= new String();

c>由於JVM內部的優化機制,JVM先畫出了一些分配給Singleton實例的空白內存,並賦值給instance成員(注意此時JVM沒有開始初始化這個實例),然後A離開了synchronized塊。

d>B進入synchronized塊,由於instance此時不是null,因此它馬上離開了synchronized塊並將結果返回給調用該方法的程序。

e>此時B線程打算使用Singleton實例,卻發現它沒有被初始化,於是錯誤發生了。

方案三:

private static class StringFactory {
        private static String str = new String(UUID.randomUUID().toString());
}
public static  String getInstance(){
        return StringFactory.str;
}

實際情況是,單例模式使用內部類來維護單例的實現,JVM內部的機制能夠保證當一個類被加載的時候,這個類的加載過程是線程互斥的。這樣當我們第一次調用getInstance的時候,JVM能夠幫我們保證instance只被創建一次,並且會保證把賦值給instance的內存初始化完畢,這樣我們就不用擔心上面的問題。同時該方法也只會在第一次調用的時候使用互斥機制,這樣就解決了低性能問題。

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