天天看點

Java多線程程式設計環境中單例模式的實作 (内部類實作多線程環境中的單例模式)...

單例模式的惰性加載

通常當我們設計一個單例類的時候,會在類的内部構造這個類(通過構造函數,或者在定義處直接建立),并對外提供一個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方法的時候才會被加載,是以也實作了惰性加載。

版權聲明:本文為CSDN部落客「weixin_34250709」的原創文章,遵循CC 4.0 BY-SA版權協定,轉載請附上原文出處連結及本聲明。

原文連結:https://blog.csdn.net/weixin_34250709/article/details/92042169