前言
在前面的文章中,我们陆续介绍了java.util.concurrent.locks包中的一些与锁相关的核心类,之前没有了解过的读者可以通过下面的链接进行查看,
聊聊并发:(九)concurrent包之ReentrantLock分析
聊聊并发:(八)concurrent包之AbstractQueuedSynchronizer源码实现分析
聊聊并发:(七)concurrent包之AbstractQueuedSynchronizer分析
本章,我们继续围绕locks包,了解一下比较常用也是比较重要的一个锁的类:ReentrantReadWriteLock。
ReentrantReadWriteLock介绍
上一篇中我们介绍了ReentrantLock,在这里可以查看:聊聊并发:(九)concurrent包之ReentrantLock分析,可能从名字上来看,与ReentrantReadWriteLock感觉是有关系的,事实上这两个锁的实现是完全没有一点关系的,是分别各自独立的实现。但与ReentrantLock一样,ReentrantReadWriteLock的实现同样是基于AQS的同步队列。
ReentrantLock与synchronized都是一种独占式锁的实现,虽然他们都支持可重入性,但是只是针对同一个拿到锁的线程来说,当多个线程争抢同一个资源的时候,只能进行排队等待其他线程释放资源,这个在并发量较大的场景下,往往可能会成为瓶颈。有一些场景下,是多个线程对同一个资源读多写少的情况,这种情况下,ReentrantReadWriteLock往往是一种更加合适的选择。
ReentrantReadWriteLock允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。
ReentrantReadWriteLock中有两种锁,第一种是readLock,另一种是writeLock,我们在下面会详细了解。
ReentrantReadWriteLock具有如下特性:
一、锁的获取顺序:
- 非公平模式(默认):当使用此种模式的时候,将不会指定进入读写锁的顺序,即一个线程获取到锁并释放后,可能立即再次获取锁,也可能导致一个线程可能一直尝试抢锁,但是获取不到,其吞吐量通常要高于公平锁。
- 公平模式:当使用此种模式的时候,线程利用一个近似到达顺序的策略来争夺进入,等待时间最长的线程将最先获取到锁,该种模式会保证获取锁的时间顺序,但是吞吐量会有所牺牲。
二、可重入性:当持有读锁的线程获取后能再次获取同一把锁,写锁获取之后能够再次获取写锁,同时也能够获取读锁,但是反之则不可以,即持有读锁的线程,不可以再次获取写锁。
三、锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,但是反之不可以,即读锁不可以升级为写锁。
四、重入数:读取锁和写入锁的数量最大分别只能是65535(包括重入数)。
上面介绍完了ReentrantReadWriteLock基本特性,接下来我们看一下其源码是如何实现的。
ReentrantReadWriteLock之锁的获取
写锁的获取:
ReentrantReadWriteLock如何获取一个写锁:
ReentrantReadWriteLock writeReentrantLock = new ReentrantReadWriteLock();
writeReentrantLock.writeLock().lock();
ReentrantReadWriteLock获取写锁的操作比较简单,直接调用其writeLock方法,获取一个写锁的实例,调用其lock方法,即可完成写锁的获取。ReentrantReadWriteLock默认使用的是非公平锁,可以在构造方法中进行指定创建“公平锁”或“非公平锁”。
上面提到过,ReentrantReadWriteLock是基于AQS实现的,这一点是与其他锁一样,去实现AQS的模板方法,自行进行实现,可以说套路都是一致的。我们来看一下源码:
ReentrantReadWriteLock:
public void lock() {
sync.acquire(1);
}
AbstractQueuedSynchronizer:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire()是一个模板方法,其具体实现在子类中进行:
ReentrantReadWriteLock:
protected final boolean tryAcquire(int acquires) {
/*
* 流程:
* 1. 如果读锁或写锁计数器不为0,且当前线程不是持有锁的线程,返回false。
* 2. 如果锁计数器即将达到阈值,返回失败。
* 3. 否则,即当前线程有资格获取锁资源,更新同步状态,并设置锁的占有者为当前线程。
*/
//获取当前线程
Thread current = Thread.currentThread();
//获取当前同步资源的状态
int c = getState();
//获取独占锁被获取的次数
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
//当独占锁被获取的次数刚好比最大限制大1时或者当前持有锁的线程不是当前线程时,返回false,进入队列
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//当独占锁被获取的次数超过最大限制数的时候,抛出异常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
//更新资源状态
setState(c + acquires);
return true;
}
//根据获取模式(公平模式或非公平模式)获取结果失败或CAS更新state失败时,返回false
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//将当前线程置为持有当前资源的线程
setExclusiveOwnerThread(current);
return true;
}
上面的代码是写锁的获取流程,细节部分可以参考注释,主流程如下:
-
- 如果读锁或写锁计数器不为0,且当前线程不是持有锁的线程,返回false。
-
- 如果锁计数器即将达到阈值,返回失败。
-
- 否则,即当前线程有资格获取锁资源,CAS更新同步状态,并设置锁的占有者为当前线程。
这里我们来分析一下独占锁被获取次数的代码:
int w = exclusiveCount(c);
看一下实现:
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
独占锁被获取次数是一个与操作计算的,EXCLUSIVE _MASK为1左移16位然后减1,即为0x0000FFFF(65535)。同步状态(state为int类型)与0x0000FFFF(65535)相与,即取同步状态的低16位。
根据exclusiveCount方法的注释为独占式获取的次数即写锁被获取的次数,现在就可以得出来一个结论:同步状态的低16位用来表示写锁的获取次数。
这里你可能会有疑问了,同步状态的低16位表示写锁的获取次数,那么高16位是干嘛的呢?
我们再来看另一个重要的方法:
/** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
根据注释,可以看到,这个方法是获取共享锁的获取次数,即读锁,该方法是将同步状态(int c)右移16次,即取同步状态的高16位,所以我们可以得出结论:同步状态的高16位用来表示读锁的获取次数。
如果看过之前系列文章的朋友可能知道,AQS系列锁的实现,内部全部使用一个state字段来控制同步状态的,但是其他锁的实现都是独占锁(ReentrantLock),即同时只有一个线程可以获取锁(重入性也是同一个线程重复拿锁),因此使用一个state字段,是没有问题的,但是读写锁中,也只有一个state字段,这个字段需要保存两个锁的同步状态,因此,设计者将state的低16位表示写锁的获取次数,高16位用来表示读锁的获取次数。
好了,了解完state是如何控制读锁与写锁的同步状态后,我们再回头仔细分析一下写锁获取的核心部分代码:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState(); // ①
int w = exclusiveCount(c); // ②
if (c != 0) {
if (w == 0 || current != getExclusiveOwnerThread()) // ③
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT) // ④
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires)) // ⑤
return false;
setExclusiveOwnerThread(current);
return true;
}
我们对写锁获取的tryAcquire()的核心几步详细分析一下:
- 步骤一:
获取同步状态,此步获取当前锁的同步状态。 - 步骤二:
获取写锁的获取次数,我们看一下具体实现:
此处将写锁的获取次数,与1左移16位-1后的数值(65535)做&计算,涉及到与计算,可能有不直观,我们带入一个数值来实际算一下:static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
假设写锁的当前获取次数为1,那么exclusiveCount()的返回值应该是:1 & ((1 << 16) -1) = 1,我们可以知道,当小于等于65535的数值,与65535做&计算,都会返回其数值,当65536与65535做&的时候,返回0;65537与65535做&的时,返回1。
OK,做完数学计算后,我们继续往下看。 - 步骤三:
当第一次获取同步状态的时候,state一定为0,那么不会进入步骤三,即只有同步状态不为0的时候,才会进入步骤三,这里会判断写锁的获取次数是否为0,上面我们说到了,只有同步状态为65536的时候,exclusiveCount()的计算结果才为0,因此步骤三为true的触发条件为:同步状态为65536,同时当前线程不是当前持有写锁的线程,此时成立,即获取写锁的线程会进入等待队列等待。 - 步骤四:
当写锁的获取次数和本次同步状态增量(其实就是1)相加,大于65535的时候,抛出异常。 - 步骤五:
writerShouldBlock()方法判断获取锁是否应该阻塞,该方法有两种实现,一种是公平锁的实现,一种是非公平锁的实现,在之前的文章中我们已经介绍过,在此不再赘述。
如果上述条件全部没有满足,即可以顺利获取写锁,将当前线程置为写锁的持有线程,返回true,不进入阻塞队列。
读锁的获取:
上面我们介绍了写锁的获取流程,接下来我们继续来看读锁的获取流程:
ReentrantReadWriteLock如何获取一个读锁:
ReentrantReadWriteLock readReentrantLock = new ReentrantReadWriteLock();
readReentrantLock.readLock().lock();
ReentrantReadWriteLock获取读锁的方式与写锁的方式差不多,调用其readLock()获取读锁对象,再调用lock()方法获取读锁。我们来看一下核心实现:
ReentrantReadWriteLock:
public void lock() {
sync.acquireShared(1);
}
AbstractQueuedSynchronizer:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
ReentrantReadWriteLock:
protected final int tryAcquireShared(int unused) {
/*
* 流程:
* 1. 如果写锁被另一个线程持有,返回失败。
* 2. 否则,当前线程对于同步状态的检查通过,接下来进行是否需要进入阻塞队列的检查(公平模式与非公平模式),如果不进入,尝试通过CAS操作更新同步状态。
* 在此步骤中,不会进行可重入的检查,该检查会推迟到fullTryAcquireShared()方法中进行。
* 3. 如果步骤二失败,则进入该检查会推迟到fullTryAcquireShared()方法重新循环尝试。
*/
Thread current = Thread.currentThread();
int c = getState();
//1、如果写锁已经被获取并且获取写锁的线程不是当前线程的话,当前线程获取读锁失败,返回-1,代表失败
//如果当前线程已经持有写锁,可以继续执行,获取读锁
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//2、获取读锁获取次数
int r = sharedCount(c);
//3、判断是否需要进入同步队列、读锁的获取次数是否达到最大限制65535、以及CAS设置同步状态是否可以成功
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//如果是首次获取读锁
if (r == 0) {
//将当前线程置为首次获取读锁的线程,并更新首次读锁获取数
firstReader = current;
firstReaderHoldCount = 1;
//如果本次获取读锁的线程是首次获取读锁的线程
} else if (firstReader == current) {
//自增首次读锁获取数
firstReaderHoldCount++;
} else {
//如果上面的情况都不是
//获取读锁计数器,如果计数器没有初始化,初始化,并将计数器计数值自增
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
//返回1,代表成功
return 1;
}
//4、如果上面条件均不满足,进入完整版获取读锁方法
return fullTryAcquireShared(current);
}
final int fullTryAcquireShared(Thread current) {
/*
* This code is in part redundant with that in
* tryAcquireShared but is simpler overall by not
* complicating tryAcquireShared with interactions between
* retries and lazily reading hold counts.
*/
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()) {
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
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);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
我们来分析一下读锁获取的流程,其实在注释中已经描述的比较详尽,读锁获取主要分为三个大步骤:
-
步骤一:
检查:如果写锁已经被获取并且获取写锁的线程不是当前线程的话,当前线程获取读锁失败,返回-1,代表失败。
如果当前线程已经持有写锁,可以继续执行,获取读锁,即锁的降级。 -
步骤二:
①获取读锁的获取次数,我们看一下读锁的获取方法:static final int SHARED_SHIFT = 16; static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
sharedCount()方法入参为同步状态值,老规矩,我们继续做数学题试试看结果。
假设写锁的当前获取次数为1,sharedCount()的返回值应该是:1 >>> = 0,我们可以知道,当小于等于65535的数值,与65535做&计算,都会返回0,当65536与65535做&的时候,返回1;65535* 65535 与65535做&的时,返回65534,我们继续往下看。②根据公平与非公平模式,判断是否需要进入同步队列,如果不需要,继续判断读锁的获取次数是否达到最大限制65535如果没有达到,判断以及CAS设置同步状态是否可以成功。这里CAS的操作我们需要注意一下,是将同步状态值加65536,更新为同步状态值。
至于为何要这么做?前面我们提到过,读写锁使用state的高低16位分别记录读锁与写锁的次数,每次将同步状态加65536再更新state,即65536 * 读锁获取次数,右移16位后,即为读锁获取的次数。
③如果是首次获取读锁,将当前线程置为首次获取读锁的线程,并初始化首次读锁获取数。
④如果非首次获取读锁,但当前获取锁的线程是首次获取锁的线程,那么将首次读锁获取数自增。
⑤如果上面的情况都不是,获取读锁计数器,如果计数器没有初始化,初始化,并将计数器计数值自增。
计数器的实现我们来看一下:static final class HoldCounter { int count = 0; // Use id, not reference, to avoid garbage retention final long tid = getThreadId(Thread.currentThread()); }
它会在锁创建的时候,通过一个ThreadLocal完成初始化:
static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> { public HoldCounter initialValue() { return new HoldCounter(); } }
-
步骤三:
如果上述条件均不满足,则进入完整版尝试获取读锁的流程,需要循环不断尝试去修改状态直到成功或者锁被写入线程占有。实际上是过程3的不断尝试直到CAS计数成功或者被写入线程占有锁。
ReentrantReadWriteLock之锁的释放
上面我们用了大量的篇幅,介绍了ReentrantReadWriteLock的内部构造机制与读锁与写锁的获取机制,下面我们来看一下读锁与写锁的释放实现。
写锁的释放:
ReentrantReadWriteLock如何释放一个写锁:
ReentrantReadWriteLock writeReentrantLock = new ReentrantReadWriteLock();
writeReentrantLock.writeLock().unlock();
ReentrantReadWriteLock:
public void unlock() {
sync.release(1);
}
AbstractQueuedSynchronizer:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
ReentrantReadWriteLock:
protected final boolean tryRelease(int releases) {
//判断当前释放锁的线程,是否是当前持有写锁的线程,如果不是,抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//减少同步状态数值
int nextc = getState() - releases;
//判断减少同步状态数值后,写锁持有的次数,是否为0
boolean free = exclusiveCount(nextc) == 0;
//如果当前持有写锁的次数为0,释放持有写锁的线程
if (free)
setExclusiveOwnerThread(null);
//更新同步状态数值
setState(nextc);
return free;
}
写锁的释放流程,相对比较简单,可以参见代码注释,唯一需要注意的一点:
减少写状态int nextc = getState() - releases;只需要用当前同步状态直接减去写状态的原因正是我们刚才所说的写状态是由同步状态的低16位表示的。
读锁的释放:
ReentrantReadWriteLock readReentrantLock = new ReentrantReadWriteLock();
readReentrantLock.readLock().unlock();
AbstractQueuedSynchronizer:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
ReentrantReadWriteLock:
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//如果当前线程是首次获取
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
老套路,通过模板方法,调用ReentrantReadWriteLock的实现,释放逻辑与加锁逻辑相对应,就不在花篇幅赘述。
需要注意的一块,同步状态值的减少,在获取锁的时候,每次都是自增65535,因此,在释放的时候,同样要减少65535。
ReentrantReadWriteLock之锁的降级
ReentrantReadWriteLock支持支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,不支持锁升级,关于锁降级下面的示例代码摘自ReentrantWriteReadLock源码中:
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
具体实现机制,可以回头看一下关于读锁的获取部分。
注意:ReentrantReadWriteLock只支持锁的降级,不支持锁的升级,即写锁可以降级为读锁,但是读锁不可以升级为写锁!
结语
本篇我们详细了解了ReentrantReadWriteLock源码实现机制,了解了读锁与写锁的使用及实现,使用ReentrantReadWriteLock可以推广到大部分读,少量写的场景,因为读线程之间没有竞争,所以比起sychronzied,性能好很多。
ReentrantReadWriteLock的设计思想非常的精妙,使用state的高低16位去记录两个状态值,这个我们可以从中受到启发。
本篇关于ReentrantReadWriteLock的介绍我们就到这里,感谢您的阅读,
下篇预告:concurrent包之condition分析,敬请期待!
更多Java干货文章请关注我的个人微信公众号:老宣与你聊Java