之前长时间的面试暴露了一些知识面不够深入,其中一块是ReentrantReadWriteLock读写锁方面,一直没有时间来深入学习,这次补上,关于ReentrantLock的内容可以看一下我前面的文章Java锁详解,里面第二部分详细的介绍了ReentrantLock,废话不多说,直接开始
从前面一文Java锁详解中,分析了以非阻塞同步算法为基础实现的可重入独占锁ReentrantLock。所谓** “独占” 即同一时间只能有一个线程持有锁。而 “重入” **是指该线程如果持有锁,可以在同步代码块内再次请求占有锁而不被阻塞,线程重入后将AQS内部状态state同步加1继续同步区的操作。但是要注意该线程要想移交锁的控制权必须完全释放重入锁,即将AQS的state同步更新到0为止。
ReentrantReadWriteLock出现的目的就是针对ReentrantLock独占带来的性能问题,使用ReentrantLock无论是“写/写”线程、“读/读”线程、“读/写”线程之间的工作都是互斥,同时只有一个线程能进入同步区域。然而大多实际场景是“读/读”线程间并不存在互斥关系,只有"读/写"线程或"写/写"线程间的操作需要互斥的。因此引入ReentrantReadWriteLock,它的特性是:** 一个资源可以被多个读操作访问,或者一个写操作访问,但两者不能同时进行。**从而提高读操作的吞吐量。
ReentrantReadWriteLock并没有继承ReentrantLock,也并没有实现Lock接口,而是实现了ReadWriteLock接口,该接口提供readLock()方法获取读锁,writeLock()获取写锁。
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
}
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
默认构造方法为** 非公平模式 ,开发者也可以通过指定fair为true设置为 公平模式 **。
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
public static class ReadLock implements Lock, java.io.Serializable {}
public static class WriteLock implements Lock, java.io.Serializable {}
而公平模式和非公平模式分别由内部类FairSync和NonfairSync实现,这两个类继承自另一个内部类Sync,该Sync继承自AbstractQueuedSynchronizer(以后简称** AQS **),这里基本同ReentrantLock的内部实现一致。
而在ReentrantLock的分析中得知,其独占性和重入性都是通过CAS操作维护AQS内部的state变量实现的。ReentrantReadWriteLock将这个int型state变量分为高16位和低16位,高16位表示当前读锁的占有量,低16位表示写锁的占有量,详见ReentrantReadWriteLock的内部类Sync :
abstract static class Sync extends AbstractQueuedSynchronizer {
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; }
...
}
读写锁分析
AQS 的状态state是32位(int 类型)的,辦成两份,读锁用高16位,表示持有读锁的线程数(sharedCount),写锁低16位,表示写锁的重入次数 (exclusiveCount)。状态值为 0 表示锁空闲,sharedCount不为 0 表示分配了读锁,exclusiveCount 不为 0 表示分配了写锁,sharedCount和exclusiveCount 一般不会同时不为 0,只有当线程占用了写锁,该线程可以重入获取读锁,反之不成立。
在写获取读写锁方法前,必须要了解俩个抽像方法:
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
writerShouldBlock和readerShouldBlock方法都表示当有别的线程也在尝试获取锁时,是否应该阻塞。
对于公平模式,hasQueuedPredecessors()方法表示前面是否有等待线程。只要AQS锁等待队列的头尾不为空,并且存在head后的节点并且节点的线程非当前线程,返回true,需要阻塞
nonfairSync的实现:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // 写线程总是可以闯入
}
final boolean readerShouldBlock() {
//
return apparentlyFirstQueuedIsExclusive();
}
}
/**如果AQS的锁等待队列head节点后的节点非共享节点(等待读锁的节点,即等待写锁),将返回true。*/
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
非公平模式下,writerShouldBlock直接返回false,说明不需要阻塞;而readShouldBlock调用了apparentFirstQueuedIsExcluisve()方法。如果等待队列中第一个等待线程想获取写锁,返回true,需要阻塞;否则返回false。
好的,我们接下来看主流程:
获取共享lock 方法 acquireShared(读锁)
public final void acquireShared(int arg){
if(tryAcquireShared(arg) < 0){ // 1. 调用子类, 获取共享 lock 返回 < 0, 表示失败
doAcquireShared(arg); // 2. 调用 doAcquireShared 当前 线程加入 Sync Queue 里面, 等待获取 lock
}
}
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1; //1.有线程持有写锁,且该线程不是当前线程,获取锁失败
int r = sharedCount(c); //2.获取读锁计数
//这里就调用了readerShouldBlock()方法,其结果参考前面的解释,如果为结果是,则直接走第4步
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {//3.如果不应该阻塞,且读锁数<MAX_COUNT且设置同步状态state成功,获取锁成功。
if (r == 0) { //下面对firstReader的处理:firstReader是不会放到readHolds里的,这样,在读锁只有一个的情况下,就避免了查找readHolds。
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
// // 非 firstReader 读锁重入计数更新
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != current.getId())
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
//4.获取读锁失败,放到循环里重试。
return fullTryAcquireShared(current);
}
进入fullTryAcquireShared(current);
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1; //1.有线程持有写锁,且该线程不是当前线程,获取锁失败
//2.有线程持有写锁,且该线程是当前线程,则应该放行让其重入获取锁,否则会造成死锁。
} else if (readerShouldBlock()) {
//3.写锁空闲 且 公平策略决定 读线程应当被阻塞
// 下面的处理是说,如果是已获取读锁的线程重入读锁时,
// 即使公平策略指示应当阻塞也不会阻塞。
// 否则,这也会导致死锁的。
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != current.getId()) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
//4.需要阻塞且是非重入(还未获取读锁的),获取失败。
if (rh.count == 0)
return -1;
}
}
//5.写锁空闲 且 公平策略决定线程可以获取读锁
if (sharedCount(c) == MAX_COUNT)//6.读锁数量达到最多
throw new Error("Maximum lock count exceeded");
//7. 申请读锁成功,下面的处理跟tryAcquireShared是类似的。
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 != current.getId())
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
到第二步doAcquireShared
private void doAcquireShared(int arg){
final Node node = addWaiter(Node.SHARED); // 1. 将当前的线程封装成 Node 加入到 Sync Queue 里面
boolean failed = true;
try {
boolean interrupted = false;
for(;;){
final Node p = node.predecessor(); // 2. 获取当前节点的前继节点 (当一个n在 Sync Queue 里面, 并且没有获取 lock 的 node 的前继节点不可能是 null)
if(p == head){
int r = tryAcquireShared(arg); // 3. 判断前继节点是否是head节点(前继节点是head, 存在两种情况 (1) 前继节点现在占用 lock (2)前继节点是个空节点, 已经释放 lock, node 现在有机会获取 lock); 则再次调用 tryAcquireShared 尝试获取一下
if(r >= 0){
setHeadAndPropagate(node, r); // 4. 获取 lock 成功, 设置新的 head, 并唤醒后继获取 readLock 的节点
p.next = null; // help GC
if(interrupted){ // 5. 在获取 lock 时, 被中断过, 则自己再自我中断一下(外面的函数可能需要这个参数)
selfInterrupt();
}
failed = false;
return;
}
}
if(shouldParkAfterFailedAcquire(p, node) && // 6. 调用 shouldParkAfterFailedAcquire 判断是否需要中断(这里可能会一开始 返回 false, 但在此进去后直接返回 true(主要和前继节点的状态是否是 signal))
parkAndCheckInterrupt()){ // 7. 现在lock还是被其他线程占用 那就睡一会, 返回值判断是否这次线程的唤醒是被中断唤醒
interrupted = true;
}
}
}finally {
if(failed){ // 8. 在整个获取中出错(比如线程中断/超时)
cancelAcquire(node); // 9. 清除 node 节点(清除的过程是先给 node 打上 CANCELLED标志, 然后再删除)
}
}
}
独占锁模式获取成功以后设置头结点然后返回中断状态,结束流程。而共享锁模式获取成功以后,调用了setHeadAndPropagate方法,从方法名就可以看出除了设置新的头结点以外还有一个传递动作,一起看下代码:
//两个入参,一个是当前成功获取共享锁的节点,一个就是tryAcquireShared方法的返回值,注意上面说的,它可能大于0也可能等于0
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; //记录当前头节点
//设置新的头节点,即把当前获取到锁的节点设置为头节点
//注:这里是获取到锁之后的操作,不需要并发控制
setHead(node);
//这里意思有两种情况是需要执行唤醒操作
//1.propagate > 0 表示调用方指明了后继节点有可能需要被唤醒,因为此方法是获取读锁过程调用,那么后面节点很可能也要获取读锁
//2.头节点后面的节点需要被唤醒(waitStatus<0),不论是老的头结点还是新的头结点
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
//如果当前节点的后继节点是共享类型获取没有后继节点,则进行唤醒
//这里可以理解为除非明确指明不需要唤醒(后继等待节点是独占类型),否则都要唤醒
//这里的初衷是 后一个节点正好是共享节点,就唤醒,实现共享,独占有锁释放时候唤醒
if (s == null || s.isShared())
//这个唤醒操作在releaseShared()方法里也会调用。唤醒后面想获取锁的节点。
doReleaseShared();
}
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
读锁加锁其过程如下:
总结:读锁的获取条件要满足:
1、** 当前的写锁未被占有(AQS state变量低16位为0) 或者当前线程是写锁占有的线程**
2、** readerShouldBlock()方法返回false **
3、** 当前读锁占有量小于最大值(2^16 -1) **
4、** 成功通过CAS操作将读锁占有量+1(AQS的state高16位同步加1) **
条件1使得读锁与写锁互斥,除非当前申请读操作的线程是占有写锁的线程,即实现了写锁降级为读锁。
条件2在非公平模式下执行的是NonfairSync类的readerShouldBlock()方法,而在公平模式下执行的是FairSync类的readerShouldBlock方法:
条件3保证读锁的占有数不超过最大上限,条件4保证多线程竞争读锁时的安全性。
不满足条件申请读锁的线程会被封装为SHARED类型的线程节点插入到AQS锁等待队列的末尾,在插入队列尾后还有一次机会尝试获取读锁。如果还是失败的,下面判断如果队列前一节点是SIGNAL状态就将线程挂起。当线程唤醒后会再次尝试获取读锁,不满足条件会再次挂起,以此循环。
** 如果在线程挂起前获取读锁,下面会将当前节点设置为head节点,并将head后的SHARED类型的节点的唤醒。然后进入读锁同步区域。被唤醒的线程会继续尝试获取读锁,获取读锁成功后就继续上述步骤,这样就保证了队列中几个连续的等待读锁的线程被依次唤醒进入读锁同步区。**
读锁的释放
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
//释放锁tryReleaseShared由子类Sync实现
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 清理firstReader缓存 或 readHolds里的重入计数
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != current.getId())
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
// 完全释放读锁
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count; // 主要用于重入退出
}
// 循环在CAS更新状态值,主要是把读锁数量减 1
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// 释放读锁对其他读线程没有任何影响,
// 但可以允许等待的写线程继续,如果读锁、写锁都空闲。
return nextc == 0;
}
}
流程如下:
读锁的释放过程即AQS的state高16位同步递减为0的过程,当state的高16位都为0表示读锁释放完毕,如果此时写锁状态为0(即该读锁不是写锁降级来的),唤醒head节点后下一个SIGNAL状态的节点的线程,一般为等待写锁的节点。如果读锁的占有数不为0,表示读锁未完全释放。或者写锁的占有数不为0,表示释放的读锁是写锁降级来的。
写锁加锁
写锁的获取和ReentrantLock独占锁的锁获取过程几乎一样,除了tryAcquire()方法,要考虑读锁的情况。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
在以下情况,写锁获取失败:
(1) 写锁为0,读锁不为0 或者写锁不为0,且当前线程不是已获取独占锁的线程,锁获取失败。
(2)写锁数量已达到最大值,写锁获取失败。
(3)当前线程应该阻塞,或者设置同步状态state失败,获取锁失败。
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// 1.写锁为0,读锁不为0 或者写锁不为0,且当前线程不是已获取独占锁的线程,锁获取失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//2. 写锁数量已达到最大值,写锁获取失败
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
//3.当前线程应该阻塞,或者设置同步状态state失败,获取锁失败。
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
流程如下:
写锁释放
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
总结:
1、读锁的重入是允许多个申请读操作的线程的,而写锁同时只允许单个线程占有,该线程的写操作可以重入。
2、如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。
3、对于同时占有读锁和写锁的线程,如果完全释放了写锁,那么它就完全转换成了读锁,以后的写操作无法重入,在写锁未完全释放时写操作是可以重入的。
4、公平模式下无论读锁还是写锁的申请都必须按照AQS锁等待队列先进先出的顺序。非公平模式下读操作插队的条件是锁等待队列head节点后的下一个节点是SHARED型节点,写锁则无条件插队。
5、读锁不允许newConditon获取Condition接口,而写锁的newCondition接口实现方法同ReentrantLock。