java開發經驗談:你真的會用單例麼?

單例是什麼?

是一種對象創建模式,可以確保項目中一個類只產生一個實例。

好處

對於頻繁使用的對象可以減少創建對象所花費的時間,這對於重量級對象來說,簡直是福音。由於new的減少,對系統內存使用頻率也會降低,減少GC的壓力,並縮短GC停頓時間,這也會減少Android項目的UI卡頓。

如何實現單例

1、餓漢模式

public class TestSingleton {

    private static final TestSingleton testSingleton = new TestSingleton();

    private TestSingleton(){

    }

    public static TestSingleton getInstance(){
        return testSingleton;
    }

}

細節我就不多寫了,大家都應該知道,構造函數爲private,用getInstance來獲取實例

2.、懶漢模式

public class TestSingleton {

    private static TestSingleton testSingleton;

    private TestSingleton(){

    }

    public static TestSingleton getInstance(){
        if(testSingleton==null){
            testSingleton = new TestSingleton();
        }
        return testSingleton;
    }

}

比餓漢式的優點在於用時再加載,比較重量級的單例,就不適用與餓漢了。

3、線程安全的懶漢模式

public class TestSingleton {

    private static TestSingleton testSingleton;

    private TestSingleton(){

    }

    public static TestSingleton getInstance(){
        if(testSingleton==null){
            synchronized (TestSingleton.class){
                testSingleton = new TestSingleton();
            }
        }
        return testSingleton;
    }

}

可以看到的是比上面的單例多了一個對象鎖,着可以保證在創建對象的時候,只有一個線程能夠創建對象。

4、線程安全的懶漢模式-DCL雙重檢查鎖機制

public class TestSingleton {

    private static volatile TestSingleton testSingleton;

    private TestSingleton(){

    }

    public static TestSingleton getInstance(){
        if(testSingleton==null){
            synchronized (TestSingleton.class){
                if(testSingleton==null){
                    testSingleton = new TestSingleton();
                }
            }
        }
        return testSingleton;
    }

}

雙重檢查,同步塊加鎖機制,保證你的單例能夠在加鎖後的代碼裏判斷空,還有增加了一個volatile 關鍵字,保證你的線程在執行指令時候按順序執行。這也是市面上見的最多的單例。

敲黑板!!知識點:原子操作、指令重排。

什麼是原子操作?

簡單來說,原子操作(atomic)就是不可分割的操作,在計算機中,就是指不會因爲線程調度被打斷的操作。

m = 6; // 這是個原子操作

假如m原先的值爲0,那麼對於這個操作,要麼執行成功m變成了6,要麼是沒執行m還是0,而不會出現諸如m=3這種中間態——即使是在併發的線程中。

而,聲明並賦值就不是一個原子操作:

int n = 6; // 這不是一個原子操作

對於這個語句,至少有兩個操作:

  1. 聲明一個變量n
  2. 給n賦值爲6

這樣就會有一箇中間狀態:變量n已經被聲明瞭但是還沒有被賦值的狀態。

在多線程中,由於線程執行順序的不確定性,如果兩個線程都使用m,就可能會導致不穩定的結果出現。

什麼是指令重排?

簡單來說,就是計算機爲了提高執行效率,會做的一些優化,在不影響最終結果的情況下,可能會對一些語句的執行順序進行調整。

int a ;   // 語句1 

a = 8 ;   // 語句2

int b = 9 ;     // 語句3

int c = a + b ; // 語句4

正常來說,對於順序結構,執行的順序是自上到下,也即1234。

但是,由於指令重排的原因,因爲不影響最終的結果,所以,實際執行的順序可能會變成3124或者1324。

由於語句3和4沒有原子性的問題,語句3和語句4也可能會拆分成原子操作,再重排。

也就是說,對於非原子性的操作,在不影響最終結果的情況下,其拆分成的原子操作可能會被重新排列執行順序。

主要在於testSingleton = new TestSingleton()這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。

  1. 給 testSingleton 分配內存
  2. 調用 testSingleton 的構造函數來初始化成員變量,形成實例
  3. 將testSingleton 對象指向分配的內存空間(執行完這步 testSingleton 纔是非 null 了)

但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,被線程二搶佔了,這時 testSingleton 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然後使用,然後順理成章地報錯。

--------------------------------------------一部分的文章可能講到如上就嘎然而止了----------------------------------------

推薦後兩種

5、靜態內部類來實現單例

public class TestSingleton {

    private TestSingleton(){

    }

    public static TestSingleton getInstance(){
        return TestSingletonInner.testSingleton;
    }

    private static class TestSingletonInner{
        static final TestSingleton testSingleton = new TestSingleton();
    }

}

static 保證數據獨一份

final 初始化完成後不能被修改,線程安全。

敲黑板!!知識點:java在加載類的時候不會將其內部的靜態內部類加載,只有在使用該內部類方法時才被調用。這明顯是最好的單例,並不需要什麼鎖一類的機制。

利用了類中靜態變量的唯一性

優點:

  1. jvm本身機制保證線程安全。
  2. synchronized 會導致性能問題。
  3. TestSingletonInner 是私有的,除了通過TestSingleton 訪問,沒有其他訪問的可能性。

6、枚舉單例

public enum  TestSingleton {

    INSTANCE;

    public void toSave(){
    }

}

使用TestSingleton.INSTANCE.toSave();

創建枚舉實例的過程是線程安全的,所以這種寫法也沒有同步的問題。如果你要自己添加一些線程安全的方法,記得控制線程安全哦。

優點:寫法簡單/線程安全

Android中的單例實際應用

1、application

本身就是單例,生命週期爲整個程序的生命週期,可以通過這個特性,能夠用來存儲一些數據

2、單例模式引起的內存泄漏

在使用Context注意用application中的context

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