[toc]
ReentrantReadWriteLock
内部类Sync简介:
前面看过AQS我们都知道,AQS中使用一个int型变量来表示锁的持有数,但是对于ReentrantReadWriteLock来说,它是分读锁和写锁,那么如何表示这两种锁的持有数呢?读写锁中采用的方式就是将int型一分为二,高16位,所有线程持有的读锁数,低16位用来表示写锁的持有个数;
读锁是一个共享锁,高16位表示的是所有线程总的持读锁数,那如何获取当前线程重入读锁数呢?这里自然引入了ThreadLocal来保存每个线程重入读锁的计数。
static final int SHARED_SHIFT = 16;//用于计数出高161位的偏移值(读锁)
static final int SHARED_UNIT = (1 << SHARED_SHIFT);//65536
//最大持锁个数
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; //用于计数出state的低16位的掩码
//该方法获取int变量高16位,即读锁持有计数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
//该方法获取int变量低16位,即写锁持有计数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
// HoldCounter变量用于存储每个线程id以及对应的读锁持有计数
static final class HoldCounter {
int count = 0;
// 使用线程id而不是线程引用,是为了防止残留
// 这里可能是考虑到强引用,垃圾对象无法回收,导致内存泄露
final long tid = getThreadId(Thread.currentThread());
}
// 当前线程持有的读锁计数,ThreadLocalHoldCounter其实就是一个ThreadLocal<HoldCounter>
private transient ThreadLocalHoldCounter readHolds;
//下面三个变量是用的比较多的变量,为了提高获取效率,就单独缓存了
// 保存上个请求读锁线程的HoldCounter。
private transient HoldCounter cachedHoldCounter;
// 保存第一个获取到读锁的线程
private transient Thread firstReader = null;
// 保存第一个获取到读锁的线程持有的读锁计数
private transient int firstReaderHoldCount;
读锁
读锁的获取
入口:
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
tryAcquireShared
方法都是交给各种同步器自己定义实现的
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//其它线程持有写锁的时候,获取读锁失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 获取读锁被持有的数量(同一个线程重入也算一次)
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {// 读锁第一次被获取,需要记录该线程和以及它的持锁记录,这里与后面线程获取读锁时,记录方式不同
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++; // 支持重入
} else {
// 非第一个获取读锁的线程,会通过readHolds来记录持有读锁树,本质上是ThreadLocal,
//里面记录线程持锁数和线程ID,不记录线程对象,是为了GC,防内存泄漏
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
// 此处使用cachedHoldCounter是为了提高效率?减少读出ThreadLocal 的次数?有待思考
// 线程第一次获锁时,readHolds.get()中的count计数为0
cachedHoldCounter = rh = readHolds.get();
// 能走到这里,说明上次缓存的线程和当前线程是同一个
//但是上次该线程的缓存锁记录为0,说明它刚释放了读锁,那这里就更新一下内部持锁计数
// 个人认为:锁释放的时候存在,虽然改了缓存,还未来得及修改readHolds的情况,这里就提前修改了
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
// 执行下面代码情况:当线程获锁遭阻(队列中有线程正在获取写锁)、读锁持有数量达上限、CAS修改state失败(毕竟并发执行)
return fullTryAcquireShared(current);
}
前面分析过,获取读锁失败时,会执行fullTryAcquireShared
,分三种情况:
1、由于队列中有线程正在获取写锁导致当前线程获取读锁失败
2、获取读锁时,CAS修改state失败(并发)
3、持读锁数已达上限
fullTryAcquireShared
方法针对这三种场景分别处理:
针对场景1:存在其它线程正在获取写锁,那么当前线程获取读锁的时候,该失败还是得失败,但是需要一些处理,防止内存泄漏:如果该线程不是第一个获取读锁的线程,且未能获取到读锁的时候,要清除它的ThreadLocal
,因为前面的代码readHolds.get()
会自动创建,不清理会内存泄漏,这个好像是为了解决一个jdk的bug,但是第一个去获取读锁的线程,不用管,它没使用ThreadLocal
;注意:如果是线程重入读锁,那就不用失败,而是自旋获取读锁。
针对场景2:CAS失败,那就通过自旋,总会成功
针对场景3:读锁数量达到上限,就抛异常。
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// 如果当前线程持有写锁,在这里又尝试获取读锁,此时队列中若有线程尝试获取写锁,当前线程会被挂起
// 此时写锁无法释放,会死锁
} 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
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
// 注意readerShouldBlock(),表示走到这里的线程应该阻塞,rh.count == 0表示未获取锁
// 这里需要把tryAcquireShared中创建的readHolds清掉,防止内存泄漏
if (rh.count == 0)
readHolds.remove();
}
}
// 走到这里,获锁失败,返回
// 需要这个判断是因为容许线程重入持有的读锁,保证它往下走
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 这里没啥好说的,结合外面for循环,自旋获取读锁,代码和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 != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
当获取锁失败的时候,调用AQS同步器方法:doAcquireShared
,作进一步处理,(注:读锁的tryAcquireShared
方法不会返回0,但是其它同步工具会,比如信号量)
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED); // 入等待队列
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
// 前驱结点为头结点,尝试获取锁
int r = tryAcquireShared(arg);
//不同同步器返回值不同,读锁不会返回0
if (r >= 0) {
//设置头结点并唤醒后继结点
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 判断获取锁失败线程是否应该挂起,如果是则挂起
// 注意内部调用的park方法能响应中断
// 如果当前线程中断后不清除标志位,线程再也无法被park
// 这里调用Thread.interrupted()会清掉中断,并在获锁成功后,将中断设置回去。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 发生异常进入,比如tryAcquireShared方法可能会抛异常
if (failed)
cancelAcquire(node);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // 保存旧的头结点
setHead(node);
// 读锁进入这里propagate肯定大于0,其它同步器,比如信号量,可能等于0,可能走到h.waitStatus < 0这个判断条件,这里需要依赖ws中间态PROPAGATE
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 这里s==null有点不清楚为什么也要走唤醒,是考虑到并发情况,后面又有线程入队了
if (s == null || s.isShared())
doReleaseShared();
}
}
读锁的释放:
入口:
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();
// 第一个持锁线程释放锁
if (firstReader == current) {
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) { //释放唯一的锁时,清除ThreadLocal,防止内存泄漏
readHolds.remove();
if (count <= 0) //未持锁线程释放锁抛异常
throw unmatchedUnlockException();
}
--rh.count;
}
//自旋修改释放后读锁的数量
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// 锁全部释放完,才去执行doReleaseShared,去唤醒
return nextc == 0;
}
}
上面提到,锁释放完才去唤醒,这是为什么呢?
举例:假设线程A,B 获取了读锁,线程C想要获取写锁,由于有线程持有读锁,线程C只能去同步队列等待,此时线程A,释放锁后,去唤醒C毫无意义,因为B还拿着读锁呢,C不可能成功的,只有等所有线程释放完了,C才能获取写锁,这期间所有要获取读锁的线程,统统排到C后面等待,给C让位,不然的话,C合适才能混出头,拿到写锁啊。(当然,人家持读锁线程,重入读锁不会阻塞喔)
释放锁成功后,自然要去唤醒队列中那些挂起的结点了,了解一下doReleaseShared
private void doReleaseShared() {
for (;;) {
Node h = head; // 缓存旧的头结点,后面用于判断头结点是否发生变化
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 复位HEAD的ws状态
continue; // loop to recheck cases
unparkSuccessor(h); // 唤醒HEAD的后置结点
}
//ws==0 有两种可能:
//情况1:HEAD结点是新的,后置结点才加入,未来得及修改HEAD的ws,如果CAS不成功,说明后继结点已修改了HEAD的ws,需要重新循环唤醒后继结点
//情况2:上面代码释放了锁,cas修改了状态,此时应该往下走
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 头结点无变化,退出循环,这里有疑问? 如果唤醒结点获锁失败再次睡眠怎么办?
// 头结点改变时,唤醒新的头结点的后继结点,会存在多个线程同时去唤醒同一个结点的情况,但是cas保证只有一个成功,
if (h == head) // loop if head changed
break;
}
}
上述方法doReleaseShared
中,if(h==head)
这个条件检测到HEAD变化,继续唤醒的做法,是为了调高唤醒效率:
比如:队列中HEAD(A)-->B-->C
当A执行doReleaseShared
时,B拿到锁,队列变成:HEAD(B)-->C
当B也执行oReleaseShared
释放锁时,如果A还未执行完doReleaseShared
,发现HEAD结点发生了变化,A和B都会去唤醒C,由于CAS,只能有一个成功。
假如B成功,A失败,那么HEAD的ws最终改为PROPAGATE
状态。
C被唤醒,C之前是阻塞在doAcquireShared
方法中:
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
此时如果C获取锁成功,会执行setHeadAndPropagate(node, r);
设置头结点,并唤醒后继,判断逻辑如下:
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0)
其实这里一堆判断逻辑和读锁关系不大了,如果是信号量,就不一样,这里就是想说一下为什么前面要设置PROPAGATE
这个状态,其实就是为了满足 h.waitStatus < 0
这个条件,唤醒后继结点,这似乎也是为了解决一个JDK bug, 有时间再去研究一下。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。