Java多線程編程環境中單例模式的實現 (內部類實現多線程環境中的單例模式)

jopen 10年前發布 | 26K 次閱讀 Java Java開發

在開發中,如果某個實例的創建需要消耗很多系統資源,那么我們通常會使用惰性加載機制,也就是說只有當使用到這個實例的時候才會創建這個實例,這個好處在單例模式中得到了廣泛應用。這個機制在single-threaded環境下的實現非常簡單,然而在multi-threaded環境下卻存在隱患。 (內部類實現多線程環境中的單例模式)單例模式的惰性加載

通常當我們設計一個單例類的時候,會在類的內部構造這個類(通過構造函數,或者在定義處直接創建),并對外提供一個static getInstance方法提供獲取該單例對象的途徑。例如:

public class Singleton      
{      
    private static Singleton instance = new Singleton();      
    private Singleton(){      
        …      
    }      
    public static Singleton getInstance(){      
             return instance;       
    }      
}
這樣的代碼缺點是:第一次加載類的時候會連帶著創建Singleton實例,這樣的結果與我們所期望的不同,因為創建實例的時候可能并不是我們需要這個實例的時候。同時如果這個Singleton實例的創建非常消耗系統資源,而應用始終都沒有使用Singleton實例,那么創建Singleton消耗的系統資源就被白白浪費了。

為了避免這種情況,我們通常使用惰性加載的機制,也就是在使用的時候才去創建。以上代碼的惰性加載代碼如下:

public class Singleton{      
    private static Singleton instance = null;      
    private Singleton(){      
        …      
    }      
    public static Singleton getInstance(){      
        if (instance == null)      
            instance = new Singleton();       
            return instance;       
    }      
}
這樣,當我們第一次調用Singleton.getInstance()的時候,這個單例才被創建,而以后再次調用的時候僅僅返回這個單例就可以了。
 
但是 惰性加載在多線程中的問題:
這是如果兩個線程A和B同時執行了該方法,然后以如下方式執行:
1.         A進入if判斷,此時foo為null,因此進入if內
2.         B進入if判斷,此時A還沒有創建foo,因此foo也為null,因此B也進入if內
3.         A創建了一個Foo并返回
4.         B也創建了一個Foo并返回
此時問題出現了,我們的單例被創建了兩次,而這并不是我們所期望的。

所以可以使用Class鎖機制 給getInstance方法加上一個synchronize前綴,這樣每次只允許一個現成調用getInstance方法:
*********PS:1

public static synchronized Singleton getInstance(){      
    if (instance == null)      
    instance = new Singleton();       
    return instance;       
}
這種解決辦法的確可以防止錯誤的出現,但是它卻很影響性能:每次調用getInstance方法的時候都必須獲得Singleton的鎖,而實際上,當單例實例被創建以后,其后的請求沒有必要再使用互斥機制了

曾經有人為了解決以上問題,提出了double-checked locking的解決方案
*********PS:2

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

讓我們來看一下這個代碼是如何工作的:首先當一個線程發出請求后,會先檢查instance是否為null,如果不是則直接返回其內容,這樣避免了進入synchronized塊所需要花費的資源。其次,即使第2節提到的情況發生了,兩個線程同時進入了第一個if判斷,那么他們也必須按照順序執行synchronized塊中的代碼,第一個進入代碼塊的線程會創建一個新的Singleton實例,而后續的線程則因為無法通過if判斷,而不會創建多余的實例。

上述描述似乎已經解決了我們面臨的所有問題,但實際上,從JVM的角度講,這些代碼仍然可能發生錯誤。

對于JVM而言,它執行的是一個個Java指令。在Java指令中創建對象和賦值操作是分開進行的,
也就是說instance = new Singleton();語句是分兩步執行的。但是JVM并不保證這兩個操作的先后順序,也就是說有可能JVM會為新的Singleton實例分配空間,然后直接賦值給instance成員,然后再去初始化這個Singleton實例。這樣就使出錯成為了可能,我們仍然以A、B兩個線程為例:

1.A、B線程同時進入了第一個if判斷
2.A首先進入synchronized塊,由于instance為null,所以它執行instance = new Singleton();
3.由于JVM內部的優化機制,JVM先畫出了一些分配給Singleton實例的空白內存,并賦值給instance成員(注意此時JVM沒有開始初始化這個實例),然后A離開了synchronized塊。
4.B進入synchronized塊,由于instance此時不是null,因此它馬上離開了synchronized塊并將結果返回給調用該方法的程序。
5.此時B線程打算使用Singleton實例,卻發現它沒有被初始化,于是錯誤發生了。

為了實現慢加載,并且不希望每次調用getInstance時都必須互斥執行,最好并且最方便的解決辦法如下:(通過內部類實現多線程環境中的單例模式)

public class Singleton{      
    private Singleton(){      
        …      
    }      
    private static class SingletonContainer{      
        private static Singleton instance = new Singleton();      
    }      
    public static Singleton getInstance(){      
        return SingletonContainer.instance;      
    }      
}




JVM內部的機制能夠保證當一個類被加載的時候,這個類的加載過程是線程互斥的。這樣當我們第一次調用getInstance的時候,JVM能夠幫我們保證instance只被創建一次,并且會保證把賦值給instance的內存初始化完畢,這樣我們就不用擔心上面的問題(PS:2)。此外該方法也只會在第一次調用的時候使用互斥機制,這樣就解決了低效問題(PS:1)。最后instance是在第一次加載SingletonContainer類時被創建的,而SingletonContainer類則在調用getInstance方法的時候才會被加載,因此也實現了惰性加載。
 
來自:http://my.oschina.net/alexgaoyh/blog/261106

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!