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

  • Post author:
  • Post category:其他




源码

在阅读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



写在最后

由衷感慨大佬们在写每一行代码时,都会想如何能写得更好,即使只是很小的优化,但收益就是这样慢慢积少成多而来的。自己要学的还有很多呀,平时也要多带着问题去思考,要注意基础知识,注意细节,多品品源码,向大佬们学习!



版权声明:本文为egg1996911原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。