快捷搜索: 王者荣耀 脱发

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()方法:

// Cant be initialized in the constructor because mocks dont 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: 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变量? // Cant be initialized in the constructor because mocks dont call the constructor. @MonotonicNonNull private volatile Object mutexDoNotUseDirectly; Inline field initialization is syntactic sugar for initializing from the constructor. (看了这篇issue我才知道,原来成员变量的直接赋值是构造函数的语法糖,实际上也属于构造函数内的一部分…惭愧TUT)
  3. 为什么不直接用synchronized (this): 我觉得单独设立一个对象实例来加锁,可以在一个对象里存在多把不同的锁,让锁的力度更细;此外,锁的是对象内部的实例,这可以避免对象外部的操作锁住对象实例本身而导致对象内部使用了synchronized (this)的行为都被影响(即与无关的行为共用了this这一把锁)。 具体可以参见该answer:

写在最后

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

经验分享 程序员 微信小程序 职场和发展