使用ReentrantReadWriteLock的好处
- 互斥锁一次只允许一个线程访问共享数据,哪怕进行的是只读操作;
- 读写锁允许对共享数据进行更高级别的并发访问;
- 对于写操作,一次只有一个线程(write线程)可以修改共享数据,对于读操作,允许任意数量的线程同时进行读取。
与互斥锁相比,使用读写锁能够提升性能则取决于读写操作期间读取数据相对于修改数据的频率,以及数据的争用——即在同一时间试图对该数据执行读取或者写入操作的线程数。
需要思考的问题
-
【问题一】AQS只有一个状态,那么如何表示 多个读锁 与 单个写锁 呢?
-
【问题二】ReentrantLock里,状态值表示重入计数,现在如何在AQS里面表示每个读锁、写锁的重入次数呢?
-
【问题三】如何实现读锁和写锁的公平性呢?
-
【答案一】一个状态是没法既表示读锁,又表示写锁的,不够用,所以就将一个状态变量分成两份用,一个变量一共32位,高16位表示读锁,低16位表示写锁。因为写锁只有一个,所以写锁的重入计数也解决了。
-
【答案二】由于读锁可以同时有多个,肯定不能再通过解决问题一的方法来解决问题二了,这里我们使用
ThreadLocal
,可以把线程重入读锁的次数作为值存放到ThreadLocal
中
ReentrantReadWriteLock类的结构
ReadLock源码 之 读锁
首先我们看一下 高16位读锁 和 低16位写锁 是如何进行分配的
创建读锁对象的操作
获取读锁对象,即将读锁锁住
该方法调用的是ReentrantReadWriteLock
类的内部类Sync
中的acquireShared()
方法,但是在Sync
中没有找到该方法,所以我们找Sync
的父类AQS
中的该方法。
我们可以看到该方法调用了两个方法,一个是tryAcquireShared
,另一个是doAcquireShared
,其中tryAcquireShared
是由AQS的实现类来实现的。接下来,我们看看tryAcquireShared
的实现。
针对获取readLock的线程的获取次数需要分3种情况
-
线程的
tid
及获取次数count
存放在HoldCounter
里面,最后放在ThreadLocal
中. -
从
cachedHoldCounter
获取存入的信息- 注意:这里不是有一个
ThreadLocl
,为什么还需要使用cacheHolderCounter
呢? - 因为:多数情况下,在进行线程
acquire readLock
后不久就会进行相应的release
,而从cachedHoldCounter
获取,省去了从ThreadLocal
中lookup
的操作(其实就是节省了资源,ThreadLocal
中查找需要遍历数组)
- 注意:这里不是有一个
-
firstReader firstReaderHoldCount
这两个属性是用来记录第一次获取锁的线程,及重入的次数。当当前线程进行释放readLock
操作后,firstReader
会被置空,当再有新的线程获取readLock
后,firstReader
就会被赋值为新的线程。
总结一下上面以共享方式获取锁的逻辑:
图片来自:并发编程网
fullTryAcquireShared
方法
final int fullTryAcquireShared(Thread current) {
/*
* 这段代码与tryAcquireShared中的代码有部分重复,但是整体更简单
*/
HoldCounter rh = null;
// 死循环
for (;;) {
int c = getState();
// 如果存在写锁
if (exclusiveCount(c) != 0) {
// 如果存在写锁,并且不是当前线程,获取锁失败,反之,如果持有写锁的是当前线程,那么就会进入下面的逻辑。
// 反之,如果存在写锁,但持有写锁的是当前线程,那么就继续尝试获取读锁。
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
// 如果不存在写锁,且可以获取读锁。
} else if (readerShouldBlock()) {
// 第一个读线程是当前线程
if (firstReader == current) {
// 如果不是当前线程
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
// 从 ThreadLocal 中取出计数器
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
// 如果读锁次数达到65535,则抛出异常
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 尝试对state 加 65536,也就是设置读锁,实际就是对高 16 位加 1
if (compareAndSetState(c, c + SHARED_UNIT)) {
// 如果读锁是空闲的
if (sharedCount(c) == 0) {
// 设置第一个读锁
firstReader = current;
// 计数器为1
firstReaderHoldCount = 1;
// 如果读锁不是空闲的,即有其他线程获取了读锁,则查看第一个线程是否是当前线程
} else if (firstReader == current) {
// 如果当前线程是第一个获取读锁的线程,则更新计数器
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
// 如果最后一个度计数器所属线程不是当前线程,则自己创建一个
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
// 对计数器+1
rh.count++;
// 更新缓存计数器
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
解释几个概念:
firstReader
:是获取读锁的第一个线程。如果只有一个线程获取读锁,很明显,使用这样一个变量速度更快。firstReaderHoldCount
:是firstReader
的计数器。- cachedHoldCounter:是最后一个获取到读锁的线程计数器,每当有新的线程获取到读锁,这个变量都会更新。这个变量的目的是:当最后一个获取读锁的线程重复获取读锁,或者释放读锁,就会直接使用这个变量,速度更快,相当于缓存。
读锁的释放
直接调用ReentrantReadWriteLock的内部类Sync中的releaseShared方法,该方法实际调用的是AQS中的releaseShared方法。
这个方法中调用了两个方法,一个是tryReleaseShared
方法,另一个是doReleaseShared
方法。
在AQS中的tryReleaseShared
方法是直接抛出异常,所以需要ReentrantReadWriteLock的内部类Sync自己实现该方法。因此,在Sync中该方法实现过程如下:
读锁释放的过程:
- 【步骤一】如果当前线程是第一个持有读锁的线程,则只需要操作firstReaderHoldCount减1,如果不是,进入步骤二;
- 【步骤二】获取到缓存计数器(最后一个线程的计数器),如果匹配到当前线程,就减少一。如果不匹配,则进入第三步;
- 【步骤三】获取当前线程自己的计数器(由于每个线程都会多次获取到锁,所以,每个线程必须保存自己的计数器);
- 【步骤四】做减1的操作
- 【步骤五】死循环修改 state 变量。
ReadLock源码 之 写锁
写锁的获取
获取写锁,这里调用了Sync
类的acquire
方法,我们之前也分析过了,在Sync
类中没有该方法,所以实际调用的是AQS
中的acquire
方法。
AQS
中的这个方法在分析ReentLock
类源码的时候分析过了,这里就不再赘述了。首先这个类里面调用了两个方法,一个是tryAcquire
,另一个是acquireQueued
方法,我们先来分析tryAcquire
方法,这个方法是调用Sync
中的tryAcquire
方法。
写锁的释放
该方法调用了ReentrantReadWriteLock内部类Sync中的release方法,但是该类中没有该方法,所以其实调用的是AQS中的release方法。
参考并感谢
[1] 并发编程网——莫那·鲁道