Guava限流器RateLimiter中mutexDoNotUseDirectly/鎖的使用

源碼

在閱讀Guava限流器源碼相關實現時,很多操作都需要加鎖,比如在setRate方法中:

  public final void setRate(double permitsPerSecond) {
    checkArgument(
        permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");
    synchronized (mutex()) {
      doSetRate(permitsPerSecond, stopwatch.readMicros());
    }
  }

上述代碼的重點即是synchronized (mutex()){},用來在真正的修改速率(doSetRate)方法前加鎖,避免出現併發問題。

接下來看mutex()方法:

  // Can't be initialized in the constructor because mocks don't call the constructor.
  @MonotonicNonNull private volatile Object mutexDoNotUseDirectly;

  private Object mutex() {
    Object mutex = mutexDoNotUseDirectly;
    if (mutex == null) {
      synchronized (this) {
        mutex = mutexDoNotUseDirectly;
        if (mutex == null) {
          mutexDoNotUseDirectly = mutex = new Object();
        }
      }
    }
    return mutex;
  }

疑惑

可以看到mutex()方法的本質就是雙重檢驗鎖的單例寫法,看到這後我內心不禁產生了很多疑問:

  1. 爲什麼不直接用synchronized (this)呢?

  2. 爲什麼要用雙重校驗鎖的懶漢單例呢,畢竟只是一個簡單的Object對象,佔用內存小,爲什麼不直接用餓漢模式初始化呢?

  3. 我們往常學習的懶漢模式,都是直接對instance本身進行操作,爲什麼這裏不直接使用mutexDoNotUseDirectly,而是要額外聲明一個局部變量mutex呢?

    通常的懶漢模式寫法:

     private Object mutex() {
        if (mutexDoNotUseDirectly == null) {
          synchronized (this) {
            if (mutexDoNotUseDirectly == null) {
              mutexDoNotUseDirectly = new Object();
            }
          }
        }
        return mutexDoNotUseDirectly;
      }
    

解惑

在查閱了相關資料後,我一一解開了自己心中的疑惑:

  1. 爲什麼要額外聲明一個局部變量mutex

    詳見issue:https://github.com/google/guava/issues/3381

    It avoids an additional volatile read of the field once it’s determined to be non-null.

    不管是初始化情況下(從4次減少到3次)或者不需要初始化的情況(從2次減少到1次)下,都能減少volatile變量(mutexDoNotUseDirectly)讀1次。

    而volatile變量在緩存中失效時,cpu需要直接去訪問內存中的最新值,訪問內存的速度顯然是不如訪問cpu自身緩存來得快,因此使用volatile變量的讀寫比使用非volatile變量成本更高。

    所以使用局部變量mutex的目的就是爲了減少volatile變量的讀次數,從而提高效率!

  2. 爲什麼不直接用餓漢模式初始化mutexDoNotUseDirectly變量?

    這一點其實作者在上面mutex()方法的註釋裏寫了:

    // Can't be initialized in the constructor because mocks don't call the constructor.
    @MonotonicNonNull private volatile Object mutexDoNotUseDirectly;
    

    原來作者是考慮到使用Mockito框架時,用mock方法創建RateLimiter的mock對象,此時RateLimiter的構造函數(包括直接賦值)都不會被執行(大家可以自己試試,的確如此),關於這點,也有相關的issue:https://github.com/google/guava/issues/3066

    Inline field initialization is syntactic sugar for initializing from the constructor.

    (看了這篇issue我才知道,原來成員變量的直接賦值是構造函數的語法糖,實際上也屬於構造函數內的一部分…慚愧TUT)

  3. 爲什麼不直接用synchronized (this)

    我覺得單獨設立一個對象實例來加鎖,可以在一個對象裏存在多把不同的鎖,讓鎖的力度更細;此外,鎖的是對象內部的實例,這可以避免對象外部的操作鎖住對象實例本身而導致對象內部使用了synchronized (this)的行爲都被影響(即與無關的行爲共用了this這一把鎖)

    具體可以參見該answer:https://stackoverflow.com/questions/12397427/what-is-different-between-method-synchronized-vs-object-synchronized

寫在最後

由衷感慨大佬們在寫每一行代碼時,都會想如何能寫得更好,即使只是很小的優化,但收益就是這樣慢慢積少成多而來的。自己要學的還有很多呀,平時也要多帶着問題去思考,要注意基礎知識,注意細節,多品品源碼,向大佬們學習!

發佈了33 篇原創文章 · 獲贊 41 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章