源码
在阅读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()
方法的本质就是双重检验锁的单例写法,看到这后我内心不禁产生了很多疑问:
-
为什么不直接用
synchronized (this)
呢? -
为什么要用双重校验锁的懒汉单例呢,毕竟只是一个简单的Object对象,占用内存小,为什么不直接用饿汉模式初始化呢?
-
我们往常学习的懒汉模式,都是直接对instance本身进行操作,为什么这里不直接使用
mutexDoNotUseDirectly
,而是要额外声明一个局部变量
mutex
呢?通常的懒汉模式写法:
private Object mutex() { if (mutexDoNotUseDirectly == null) { synchronized (this) { if (mutexDoNotUseDirectly == null) { mutexDoNotUseDirectly = new Object(); } } } return mutexDoNotUseDirectly; }
解惑
在查阅了相关资料后,我一一解开了自己心中的疑惑:
-
为什么要额外声明一个局部变量
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变量的读次数,从而提高效率!
-
为什么不直接用饿汉模式初始化
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)
-
为什么不直接用
synchronized (this)
:我觉得单独设立一个对象实例来加锁,可以在一个对象里存在多把不同的锁,
让锁的力度更细
;此外,锁的是对象内部的实例,这可以
避免对象外部的操作锁住对象实例本身而导致对象内部使用了
synchronized (this)
的行为都被影响(即与无关的行为共用了this这一把锁)
。具体可以参见该answer:
https://stackoverflow.com/questions/12397427/what-is-different-between-method-synchronized-vs-object-synchronized
写在最后
由衷感慨大佬们在写每一行代码时,都会想如何能写得更好,即使只是很小的优化,但收益就是这样慢慢积少成多而来的。自己要学的还有很多呀,平时也要多带着问题去思考,要注意基础知识,注意细节,多品品源码,向大佬们学习!