EventBus框架源碼解析上(單例模式)

轉載請標明出處:【顧林海的博客】

前言

EventBus能夠簡化各組件間的通信,讓我們的代碼書寫變得簡單,能有效的分離事件發送方和接收方(也就是解耦的意思),能避免複雜和容易出錯的依賴性和生命週期問題。關於它的使用方式,同學們可以查看相關文章。

EventBus.getDefault().register(this);

以上是EventBus的註冊,很簡單,通過getDefault()方法獲取EventBus實例,再通過它的register(Object subscriber)方法註冊事件接受的類。

我們爲什麼要去看EventBus的源碼呢?一是爲了瞭解它的實現原理,二是學習作者的代碼編寫時的思想包括用到的設計模式,當然更重要的是學習到大神們的編程思維(知識),進而提升自己的代碼水平(工資)。從getDefault()方法開始,代碼如下:

public class EventBus {

    static volatile EventBus defaultInstance;

    public static EventBus getDefault() {
        if (defaultInstance == null) {
            synchronized (EventBus.class) {
                if (defaultInstance == null) {
                    defaultInstance = new EventBus();
                }
            }
        }
        return defaultInstance;
    }

}

看到上面的代碼-單例,相信大家在很多開源項目中都用到過,比如圖片加載框架Picasso和Glide,爲什麼以上這種創建對象的方式在一些著名的開源框架中被運用,這是我們關注的,
就像上面的代碼中,爲什麼這麼寫?首先要了解DCL(雙重檢查鎖定)和volatile的相關概念。

DCL(雙重檢查鎖定)

相對於我們前端開發中,很少,甚至都接觸不到多線程相關的知識,DCL(雙重檢查鎖定)就是在多線程中,用於延遲初始化來降低初始化類和創建類的開銷。在創建對象時,如果對象初始化操作
需要高開銷,這時會採用一些延遲化初始化方案,比如DCL(雙重檢查鎖定),比如,下面就是一個非線程安全的延遲初始化對象的實例。

public class Demo {
    private static Demo mDemo;

    public static Demo getInstance() {
        if (null == mDemo) {//1
            mDemo = new Demo();//2
        }
        return mDemo;
    }
}

其實上面代碼並沒有什麼問題(單線程),但如果在多線程的情況下,就有可能出現問題,我們來分析下問題出在哪裏,假設當前有兩個線程A和B,並且這兩個線程都執行了上面
這段代碼,如果A線程執行到代碼1,B線程也執行到代碼1,此時mDemo引用的對象都爲空,接下來B線程執行代碼2,對象創建完畢,這時回過頭看線程A,由於之前線程
A在判斷mDemo引用的對象爲空,線程A就會執行到代碼2,可以發現mDemo對象創建了兩次,造成額外的開銷。

如何去解決這個問題呢,可以對這個方法進行同步處理,使用synchronized。代碼如下:

public class Demo {
    private static Demo mDemo;

    public synchronized static Demo getInstance() {
        if (null == mDemo) {//1
            mDemo = new Demo();//2
        }
        return mDemo;
    }
}

使用這種方式在多線程頻繁調用的情況下,會導致程序性能的下降,此時DCL(雙重檢查鎖定)登場,代碼如下:

public class Demo {
    private static Demo mDemo;

    public static Demo getInstance() {
        if (null == mDemo) {//1
            synchronized (Demo.class) {//2
                if (null == mDemo) {//3
                    mDemo = new Demo();//4
                }
            }

        }
        return mDemo;
    }
}

通過DCL可以大幅降低synchronized帶來的性能開銷,在多線程中試圖在同一時刻創建對象時,會通過加鎖來保證只有一個線程能創建對象,同時在對象創建
完畢後,執行getInstance()方法不需要獲取鎖,直接返回已創建好的對象。
使用DCL看起來很完美,但遇到重排序的問題時,就會出現錯誤,比如在代碼1處的mDemo不爲null時,mDemo引用的對象有可能還沒有完成初始化。

重排序

重排序是指編譯器和處理器爲了優化程序性能而對指令序列進行重新排序的一種手段。編譯器和處理器在做重排序時,會遵守數據依賴性,也就是說不會改變
存在數據依賴關係的兩個操作的執行順序,數據依賴性僅僅針對單個處理器中執行的指令序列和單個線程中執行的操作。

回過頭看上面代碼中的代碼4處可以分解爲3步:

  • 第一步:分配對象的內存空間
  • 第二步:初始化對象
  • 第三步:設置mDemo指向剛分配的內存地址

當遇到重排序的問題是,第二步和第三步就會重排序,重排序後的順序如下:

  • 第一步:分配對象的內存空間
  • 第三步:設置mDemo指向剛分配的內存地址
  • 第二步:初始化對象

第二步和第三步重排序並不會改變單線程內的程序執行結果,這裏還是以兩個線程A和B爲例,它們的時間線如下:

  • A1:分配對象的內存空間
  • A3:設置mDemo指向剛分配的內存地址
  • B1:判斷mDemo是否爲空
  • B2:mDemo不爲空,線程B訪問mDemo引用的對象
  • A2:初始化對象
  • A4:訪問mDemo引用的對象

從線程A和B的時間線中可以看出,執行線程B時,由於線程A進行了重排序,導致mDemo在沒有初始化對象時就已經分配了內存地址時,從而線程B執行代碼1處
發現mDemo不爲空,從而獲取了一個未初始化的對象。

volatile

既然DCL的問題已經出現了,那我們總歸要解決它,給出的方案是基於volatile的解決方案(JDK5或更高版本)。只需要修改代碼如下:

public class Demo {
    private volatile static Demo mDemo;

    public static Demo getInstance() {
        if (null == mDemo) {//1
            synchronized (Demo.class) {//2
                if (null == mDemo) {//3
                    mDemo = new Demo();//4
                }
            }

        }
        return mDemo;
    }
}

當申明的對象爲volatile時,在多線程中重排序會被禁止,從而解決了DCL帶來的重排序問題。

volatile的主要作用是使變量在多個線程間可見,首先我們要明白在在線程中創建的變量會被存放在兩個堆棧中,分別是:
+ 公共堆棧
+ 線程的私有堆棧

當我們在主線程中設置某個線程中的變量,該線程中的變量會從私有堆棧中獲取,而主線中給該線程設置的值被更新到公共堆棧中,這樣的話會導致私有
堆棧和公共堆棧數據不同步的問題,通過使用volatile關鍵字,可以強制的從公共內存中讀取變量,從而保持線程間訪問某個變量時數據同步,volatile
增加了實例變量在多個線程間的可見性。

volatile的一個比較明顯的缺點是不支持原子性,它解決的是變量在多個線程之間的可見性,而synchronized關鍵字解決的是多個線程間訪問資源的同步性。關於
volatile就介紹到這裏,感興趣的同學可以閱讀Java併發編程的藝術這本書。

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