在Java并发包中,有独占模式的锁,同时只允许一个线程执行,阻塞其它所有线程的读写操作;还有共享模式的锁,它允许同时有多个线程进行读操作,但是会阻塞其它线程的写操作。典型的实现就是读写锁——ReentrantReadWriteLock,下面我们来分析一下它的实现原理。
读写状态的设计
如果阅读过我的上一篇文章《ReentrantLock源码分析》可以知道:AQS中有一个state字段表示锁的获取状态,或者说是重入了几次,那么对于读写锁,它既要表示出读锁的重入次数,又要表示写锁的重入次数,用一个int值怎样表示?我们知道int类型有32位,我们可以把它拆为两半,高16位表示读状态,低16位表示写状态,如图:
那么读写锁又是怎样分别获取读和写的状态?答案是通过位运算,如果想获取写状态,只需要将高16位全部抹0即可,也就是state & 0x0000FFFF,如果要获取读状态,只需将高16位无符号右移16位即可,也就是state >>> 16。如果写状态+1,直接state+1,如果读状态+1,那么就需要state + 0x00010000。
以上便是读写状态的设计原理,我们再来看看源码中是怎么写的:
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; }
在ReentrantReadWriteLock中有一个静态内部类Sync,这个Sync中又定义了上面几个字段,其中:
SHARED_SHIFT
可以理解为移位的位数;
SHARED_UNIT
就是在读状态+1时,state相加的那个值,1左移16位便是0x00010000;
MAX_COUNT
可以代表读锁或者写锁的最大重入次数,1左移16位再-1等于0x0000FFFF,也就是16bit表示的最大值,无论是读还是写都不能超过这个值;
EXCLUSIVE_MASK
就是在获取写状态时进行&运算的掩码,同为0x0000FFFF。
紧接着还有两个方法:
sharedCount
计算读状态,直接把参数c无符号右移16位;
exclusiveCount
计算写状态,将参数c和EXCLUSIVE_MASK做&运算。
跟刚才所述的原理是相符的。
写锁的加锁过程
在ReentrantReadWriteLock中有一个静态内部类WriteLock即使写锁,它的lock方法调用了sync.acquire(1),其中acquire方法中又调用了tryAcquire,我们直接来看Sync中tryAcquire的实现(这些步骤在我的上一篇文章中已有详细描述,此处不再赘述):
protected final boolean tryAcquire(int acquires) {
//获取当前线程
Thread current = Thread.currentThread();
//获取state值
int c = getState();
//获取写状态
int w = exclusiveCount(c);
if (c != 0) {
//如果state不为0,且写状态为0,说明有线程获取了读锁,此时要阻塞写锁,直接返回false
//如果state不为0,写状态也不为0,说明有线程持有写锁,但是当前线程不是持有锁的线程,也返回false
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//如果有线程持有写锁且等于当前线程,则重入
setState(c + acquires);
return true;
}
//如果没有线程持有锁,则先判断当前线程是否需要被阻塞
if (writerShouldBlock() ||
//如果不需要被阻塞,则利用CAS设置state值
!compareAndSetState(c, c + acquires))
return false;
//获取锁成功,将独占线程设置为当前线程
setExclusiveOwnerThread(current);
return true;
}
上述代码,需要关注一下writerShouldBlock()方法,这是一个在Sync中定义的抽象方法,具体实现交由FairSync和NonfairSync,FairSync中实现如下:
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
hasQueuedPredecessors()方法的解析请参考上一篇文章ReentrantLock源码分析。
NonfairSync中实现:
final boolean writerShouldBlock() {
return false; // writers can always barge
}
可见非公平锁直接返回false,直接竞争锁,从来不需要阻塞。
写锁的解锁过程
WriteLock的unlock方法调用了sync.release(1),release方法如下:
public final boolean release(int arg) {
//尝试释放锁
if (tryRelease(arg)) {
//如果释放锁成功,获取头结点
Node h = head;
//如果队列被初始化,头不为null
//并且头结点的waitStatus不为0,说明有后置结点(后置节点会把前一个结点状态置为SIGNAL,原因请参见上篇文章)
if (h != null && h.waitStatus != 0)
//唤醒后置结点
unparkSuccessor(h);
return true;
}
return false;
}
unparkSuccessor在上篇文章中已经解析过了,我们重点看看其中的tryRelease方法:
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//state减去相应的值
int nextc = getState() - releases;
//获取写锁状态并判断是否为0
boolean free = exclusiveCount(nextc) == 0;
if (free)
//如果没有独占线程,则将exclusiveOwnerThread置为null
setExclusiveOwnerThread(null);
//重新设置state
setState(nextc);
return free;
}
其实对于写锁,它的很多处理逻辑与ReentrantLock是类似的,重点还是ReadLock,我们接下来重点分析一下读锁的逻辑。
读锁的加锁过程(重点)
找到ReadLock的lock方法:
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
与独占锁不同的是,这里调用tryAcquireShared尝试获取锁。Sync中tryAcquireShared的实现:
protected final int tryAcquireShared(int unused) {
//获取当前线程
Thread current = Thread.currentThread();
//获取state
int c = getState();
//获取独占锁的状态
if (exclusiveCount(c) != 0 &&
//如果有线程持有独占锁并且不是当前线程,返回-1,获取锁失败
getExclusiveOwnerThread() != current)
return -1;
//获取共享锁状态
int r = sharedCount(c);
//判断当前线程是否需要阻塞
if (!readerShouldBlock() &&
//判断读锁状态是否小于最大可重入次数
r < MAX_COUNT &&
//CAS修改读锁state
compareAndSetState(c, c + SHARED_UNIT)) {
//如果CAS修改成功,判断当前读锁状态是否为0,如果是说明是第一次获取读锁
if (r == 0) {
//这两个变量相当于对第一个读锁的信息缓存(下面会详细说明)
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
//如果当前线程是第一个获取读锁的线程,则重入次数+1
firstReaderHoldCount++;
} else {
//HoldCounter可以理解为除了第一个获取读锁线程外的读线程的信息缓存(下面详细解释)
HoldCounter rh = cachedHoldCounter;
//如果rh为null或者rh中缓存的线程id不等于当前线程id
if (rh == null || rh.tid != getThreadId(current))
//获取当前线程的HoldCounter
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
我们看看里面调用的readerShouldBlock方法,这也是一个抽象方法,在FairSync中它的实现是调用了hasQueuedPredecessors方法(上篇文章讲过),NonfairSync中的实现是调用了一个apparentlyFirstQueuedIsExclusive方法:
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
这个方法顾名思义,判断第一个排队的节点是否是独占模式的。这里有一个问题,为什么获取头结点的下一个节点?如果看过我的上一篇文章应该知道:AQS中第一个排队的节点其实是第二个节点,头结点是已经获取锁的节点,就好比火车站售票窗口前,第一个人是正在买票的,第二个人开始才是在排队的。这也就印证了该方法的命名。
然后再判断当前读锁状态是否小于最大可重入次数,如果是则尝试CAS修改状态,如果修改成功,则执行下面的逻辑。
我们看到下面的逻辑中,有几个新面孔:firstReader、firstReaderHoldCount、HoldCounter、readHolds、cachedHoldCounter,这些是什么?
先思考一个问题:如果是独占锁,同一时间只有一个线程可以获取锁,state只需表示这个线程的重入次数即可;但是对于共享锁,它是允许多个读线程同时持有锁的,这时state虽然可以表示总共的重入次数,但对于每个读线程的重入次数怎么表示?此时一个state就无能为力了,这就需要有两个额外的变量,一个存储某读线程(或线程id),另一个存储该线程获取读锁的次数。
所以firstReader、firstReaderHoldCount就是表示第一个获取读锁的线程和它的重入次数。
那么问题又来了,除了第一个读线程,其它线程的这两个变量怎么定义?难不成定义N组变量,每来一个线程就用一组?显然不合理。这时候你要想到ThreadLocal这个东西,将每个读线程的信息绑定到它自己内部,这就巧妙的解决了这个问题,这便是readHolds:
/**
* The number of reentrant read locks held by current thread.
* Initialized only in constructor and readObject.
* Removed whenever a thread's read hold count drops to 0.
*/
private transient ThreadLocalHoldCounter readHolds;
ThreadLocalHoldCounter定义:
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
它继承自ThreadLocal,并重写了initialValue方法,这样即使没有set过值,第一次get也可以获取一个初始化的HoldCounter对象。那么HoldCounter又是什么:
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
里面就两个成员变量,count表示线程重入次数,tid表示线程id,这刚好印证了我刚才的论述。
最后还有cachedHoldCounter,它也是HoldCounter类型的对象,这个可以理解为最后一次获取读锁的线程缓存信息。
所以,下面的逻辑就是:如果r==0说明没有线程获取读锁,当前线程就是firstReader,将firstReader和firstReaderHoldCount赋值;如果firstReader有值且等于当前线程,说明firstReader又重入了一次,firstReaderHoldCount++;如果都不是,再获取cachedHoldCounter,也就是最近一次获取读锁的线程信息,如果它为null或者它缓存的线程id不是当前线程的id,则通过readHolds获取一个当前线程的HoldCounter并刷新cachedHoldCounter。所有步骤完成后,会返回1。
我们再回到acquireShared方法:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
通过刚才的分析我们可以得知,如果tryAcquireShared返回小于0的值说明获取锁失败,返回大于0的值获取锁成功,如果获取锁失败了,则应该把当前线程放到队列中去,这就是doAcquireShared所做的事:
private void doAcquireShared(int arg) {
//以共享模式创建一个Node,并至于队列尾部(上篇文章分析过)
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
//这里是一个死循环
for (;;) {
//获取当前节点的前一个节点p
final Node p = node.predecessor();
if (p == head) {
//如果p是头节点,再尝试获取一次锁
int r = tryAcquireShared(arg);
if (r >= 0) {
//如果这次获取成功,设置新的头节点并向后传播(下面会详细解释)
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//判断是否需要挂起,并且将前一个节点状态置为SIGNAL(上篇文章解释过)
if (shouldParkAfterFailedAcquire(p, node) &&
//挂起线程
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这里需要重点关注下setHeadAndPropagate方法:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
//将头结点设置为当前线程的node
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
//获取当前节点的下一个节点
Node s = node.next;
//如果下一个节点为null或者是共享模式的,则需要被唤醒,或者设置当前节点模式为PROPAGATE
if (s == null || s.isShared())
doReleaseShared();
}
}
doReleaseShared:
private void doReleaseShared() {
//此处是一个死循环
for (;;) {
//此处获取的头结点是刚刚被设置的新Node
Node h = head;
if (h != null && h != tail) {
//获取头结点的waitStatus
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
//如果头节点的状态为SIGNAL,说明后面还有节点(因为后面的节点会把前置节点状态设为SIGNAL,上篇文章解释过)
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
//如果CAS成功是不执行continue的,如果失败了再循环一次,执行重复逻辑
continue; // loop to recheck cases
//唤醒后面的节点(上篇文章解释过)
unparkSuccessor(h);
}
//如果ws是0,说明没有后置节点
else if (ws == 0 &&
//利用CAS将头结点状态设为PROPAGATE,方便将状态传播给以后进来的节点
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
//如果h不等于head,说明中间有其他共享节点进入了队列,头节点发生了变化,那就还需要再循环执行上述逻辑
//如果h等于head,直接跳出循环
if (h == head) // loop if head changed
break;
}
}
这里有一个疑惑,为什么ws==0时,要将头节点状态设为PROPAGATE?shouldParkAfterFailedAcquire方法中有这样一段代码:
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
看注释的意思,如果waitStatus是0或者PROPAGATE,则将waitStatus设为SIGNAL,所以我的理解就是为了让以后来的节点把自己设为SIGNAL,先把自己置为PROPAGATE。但是如果waitStatus本身为0,后来节点也会把它置为SIGNAL的,所以此处到底用意何在,我也不是很明白,希望有明白的读者指点一二。
这里再说一嘴:为什么头节点的下一个节点是共享节点要唤醒?因为共享锁本来就允许多个读线程同时拥有锁,所以头节点要唤醒后续所有连续的共享节点。当然,这里只调用了一次unparkSuccessor方法,也就是说头节点只能唤醒紧跟在后面的一个节点,如果后面还有共享节点呢?没关系,下一个节点被唤醒后,会从被阻塞的代码处开始执行,也就是doAcquireShared中调用parkAndCheckInterrupt的地方,被唤醒后又进入一次循环,循环中又会走一次调用链setHeadAndPropagate -> doReleaseShared -> unparkSuccessor,这样就一个接一个把后续共享节点唤醒了。
至此,ReadLock的加锁过程分析完毕,还是非常复杂的,个人认为读锁的加锁过程是整个ReentrantReadWriteLock最精华的部分。
读锁的解锁过程
ReadLock的unlock方法:
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
先来看tryReleaseShared实现:
protected final boolean tryReleaseShared(int unused) {
//获取当前线程
Thread current = Thread.currentThread();
//如果firstReader是当前线程
if (firstReader == current) {
if (firstReaderHoldCount == 1)
//如果firstReaderHoldCount只剩一次,说明本次释放锁后firstReader将不再持有锁,故将firstReader置为null
firstReader = null;
else
//否则firstReaderHoldCount减1
firstReaderHoldCount--;
} else {
//如果firstReader不是当前线程,获取最后一次获取读锁的线程缓存信息
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
//如果rh.tid不是当前线程id,则获取当前线程绑定的rh
rh = readHolds.get();
//获取rh缓存的重入重入次数
int count = rh.count;
if (count <= 1) {
//如果重入次数已经<=1,直接移除缓存信息
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
//重入次数-1
--rh.count;
}
for (;;) {
//获取当前state
int c = getState();
//读锁状态-1
int nextc = c - SHARED_UNIT;
//CAS设置新状态
if (compareAndSetState(c, nextc))
//如果nextc为0,说明当前线程已完全释放读锁,返回true,否则返回false
return nextc == 0;
}
}
回到releaseShared,如果tryReleaseShared返回true,释放锁成功,则调用doReleaseShared,doReleaseShared刚刚我们分析过,只不过这次它是为了唤醒后面的一个写线程。
至此,ReentrantReadWriteLock分析完毕。