/ 今日科技快讯 /
据《环球时报》援引印度当地媒体报道称,印度政府于29日在一份新闻稿中说,印度出于“安全”考虑,禁止包括TikTok和微信在内的59款中国应用App,认为这些应用从事的活动有损印度主权、国防、国家安全和公共秩序。
/ 作者简介 /
本篇文章来自小猪快跑22的投稿,分享了他对可重入读写锁源码的解析,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!
小猪快跑22博客地址:
https://me.csdn.net/zhujiangtaotaise
/ 前言 /
ReentrantReadWriteLock的实现也是基于AQS实现的,代码也不是很复杂,下面开讲。
/ 简介 /
之前讲到AQS有2个最重要的属性,state, 它是int型的,表示加锁的状态,初始状态值为0;另外一个是exclusiveOwnerThread,它表示的是获得锁的线程,也叫独占线程。AQS中还有一个用来存储获取锁失败线程的队列,以及head和tail结点。
ReentrantReadWriteLock同样的有一个内部类Sync实现了AQS,同样的有NonfairSync和FairSync扩展了AQS。我这里还是只讲NonfairSync,原理都差不错,所以不赘述FairSync了。
注:
我这里为了方便调试打断点和添加log,所以把代码copy一份到本地,且命名为 MyReentrantReadWriteLock,由于里面用到了CAS,即需要用到Unsafe类,由于Unsafe类在我们自己写的类中是无法导入的,所以我利用反射写了一个 UnsafeUtils.java,需要的可以在底下留言,我会把完整代码发给你。
/ 简要说明 /
ReentrantReadWriteLock 用state的高16位作为读锁的数量,低16位表示写锁的数量。多个线程是可以同时获取读锁而不需要阻塞等待;一个获取写锁的线程是可以在释放写锁之前再次获取读锁的,这就是锁降级。一个线程获取了读锁,那么其他的线程要获取写锁 需要等待;同样的,一个线程获取了写锁,另外的想要获取读锁或者写锁都需要阻塞等待。
/ 读锁的获取和释放 /
看下我写的demo ,demo很简单,就是A、B、C三个线程获取读锁,read代码如下:
public void read() {
try {
readLock.lock();
System.out.println(Thread.currentThread().getName() + " 获取读锁");
Thread.sleep(3 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + " 释放读锁");
readLock.unlock();
}
}
线程调用的代码如下:
new Thread("A") {
@Override
public void run() {
testMyReadWriteLock.read();
}
}.start();
sleep10ms();
new Thread("B") {
@Override
public void run() {
testMyReadWriteLock.read();
}
}.start();
sleep10ms();
new Thread("C"){
@Override
public void run() {
testMyReadWriteLock.read();
}
}.start();
其中sleep10ms()这个方法就是让main线程休息10ms,这样做的目的是为了log,打印的更加清晰。
read方法中,readLock.lock(); 就是获取读锁,调用的就是ReentrantReadWriteLock的lock方法,如下:
public void lock() {
sync.acquireShared(1);
}
acquireShared 调用的就是AQS的 acquireShared 方法了,如下:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
tryAcquireShared就是在AQS的实现类中重写了,tryAcquireShared小于0 表示 有可能需要加入到等待队列中了。为什么是有可能而不是一定会加入到等待队列,这个下面的源码分析中会说。
tryAcquireShared就是在ReentrantReadWriteLock的内部类Sync中重写了,如下:
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
// 获取 state 的值
int c = getState();
System.out.println("c = " + c + ", threadName = " + current.getName() +", tid = "+current.getId());
// exclusiveCount(c) != 0 表示有线程获取了写锁
// 如果有线程获取了写锁,且获取写锁的线程不是当前正在获取读锁的线程,那么直接return -1,表示获取读锁失败,获取锁失败的后续逻辑下面再讲
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
// 因为 state 的高16位表示读锁的获取次数,低16位表示写锁的次数
// r 等于 state 右移 16位,相当于读锁的次数
int r = sharedCount(c);
System.out.println("r = " + r);
// readerShouldBlock 默认是false , SHARED_UNIT = 65536
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// r = 0 表示是第一个获取读锁的线程,记录下 firstReader 等于当前线程
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) { // 当前获取了读锁的线程调用另外一个需要获取读锁的方法,那么 第一个度线程计算加1
firstReaderHoldCount++;
} else {
// 如果是另外的线程来获取读锁,那么 利用 cachedHoldCounter来记录,cachedHoldCounter 永远保存的是最后一个获取读锁的线程信息
HoldCounter rh = cachedHoldCounter;
System.out.println("rh = "+rh+" tid = " + (rh != null ? rh.tid : "[rh == null]"));
if (rh == null || rh.tid != getThreadId(current)){
// 注意,这里面readHolds用的是ThreadLocal, get 获取初始值的同时会将值设置为当前线程的ThreadLocal中
cachedHoldCounter = rh = readHolds.get();
} else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
// 需要被阻塞或者读锁计数器大于等于65535 ,或者 CAS 设置state失败,那么需要进入下面完整版的获取锁的过程
return fullTryAcquireShared(current);
}
上面的 exclusiveCount 方法就是 将 state 右移 16位,来获取state高16位的值,也就是读锁的次数,之前说了 state 的高16位表示的是读锁的次数。代码如下:
static int sharedCount(int c) {
return c >>> SHARED_SHIFT; // SHARED_SHIFT = 16
}
getExclusiveOwnerThread:表示的是独占的线程,ReentrantReadWriteLock中只有获取写锁的时候才会设置独占线程,读锁不会设置。
compareAndSetState(c, c + SHARED_UNIT):就是通过CAS将值赋值给 state,CAS操作不了解的话看看我之前的文章。
SHARED_UNIT = 65536,假设有3个线程(A、B、C)要来获取读锁,A先获取, 那么state通过cas操作后变成65536,二进制表示就是0000 0000 0000 0001 0000 0000 0000 0000,是用高16位来表示读锁的次数即读锁的次数是1,可以得知A获取读锁后,获取读锁的次数是1。
B同样通过cas操作将state的值设置为65536 + 65536 ,二进制表示为 0000 0000 0000 0010 0000 0000 0000 0000,表示 B 获取读锁后,读锁的次数等于2,
C线程获取读锁成功后,同样cas操作后state的值等于之前的值131072 + 65556,用二进制表示为:0000 0000 0000 0011 0000 0000 0000 0000, 可以看出C获取读锁后读锁的次数变为3。
然后举例来分析如下的代码:
if (r == 0) { // **注1**
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) { // **注2**
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;// **注3**
System.out.println("rh = "+rh+" tid = " + (rh != null ? rh.tid : "[rh == null]"));
if (rh == null || rh.tid != getThreadId(current)){ // **注4**
// 注意,这里面readHolds用的是ThreadLocal, get 获取初始值的同时会将值设置为当前线程的ThreadLocal中
cachedHoldCounter = rh = readHolds.get(); //**注5**
} else if (rh.count == 0) // **注6**
readHolds.set(rh); // **注7**
rh.count++;// **注8**
}
假设有3个线程来获取读锁,分别为A、B、C,且假设A、B、C 的间隔是10ms,来分析看看:
r表示的是获取读锁的次数,当线程A来获取的时候r = 0,表示A线程是第一个获取读锁的线程,将firstReader线程赋值为线程A,第一个线程获取读锁的次数记为firstReaderHoldCount,且赋值为0;
假设A线程现在要调用另外一个要获取读锁的方法,随便举个例子,帮组大家更好的理解:
public void read2() {
try {
readLock.lock();
...
read3()
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
public void read3() {
try {
readLock.lock();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
A 线程 执行read2方法获取了读锁后,要接着调用read3方法去获取读锁,因为A之前获取了一次读锁,那么r = 1,所以会走到注释2,比较firstReader 是不是当前线程,因为之前firstReader赋值为线程A,所以,条件成立,那么第一个线程获取读锁次数会加1,那么firstReaderHoldCount = 2
现在B线程来获取读锁,计算出 r = 2,走到注释2,发现第一个读线程 firstReader(等于A线程)不等于线程B,走到注释3,HoldCounter rh = cachedHoldCounter; 由于是第一次执行这里,cachedHoldCounter = null,所以,cachedHoldCounter通过readHolds.get()来赋值,readHolds是利用ThreadLocal来实现的,这样就可以保证在各个线程中获取的值是唯一的;然后rh.count++表示当前线程获取的次数增加一次。
注:cachedHoldCounter是HoldCounter类型的,HoldCounter是ReentrantReadWriteLock的内部类,有两个值,count:表示当前线程获取读锁的次数;tid:记录了当前线程的id; cachedHoldCounter保存的是最后一个获取读锁的线程信息。
现在轮到线程C来获取读锁了,经计算r = 3,因为 之前A获取了2次,B获取了1次;执行到注释3,同样的firstReader不等于线程C,所以 ,走到注释3,HoldCounter rh = cachedHoldCounter;rh赋值为线程B的HoldCounter类型的值,走到注释4的if语句, 次数rh不等于null, 但是rh.tid != getThreadId(current)是成立的,rh.tid表示的是线程B的id, 而current表示的是线程c的id,所以不相等。然后执行注释5,即线程C的锁信息也是通过ThreadLocal重新赋值的。然后线程C的读锁次数count++,rh.count++;
现在假设线程C和线程A一样也要调用一个获取读锁的方法,同理会走到 注释3,rh 赋值为 最后获取锁线程的所信息即线程C的锁信息,然后走到if语句 rh.tid != getThreadId(current) 不成立,因为都是线程C,所以走到 注释8 rh.count++,即线程C的所信息中获取读锁的次数加1。
里面的代码不多也不复杂,为什么我分析这么多呢?凑字数吗,肯定不是,其实是分析了各行代码出现的场景,什么之后执行到改行代码:流程图如下:
场景2 多线程获取读锁和写锁
这里场景也比较多,包括:
线程A获取了读锁且未释放 然后线程B 去获取写锁
线程A获取了写锁且未释放再去获取读锁
线程A获取了读锁再去申请写锁
线程A获取了写锁且未释放,线程B去获取读锁
线程A获取了写锁未释放 然后线程B也要去获取写锁
线程A获取读锁未释放 线程B接着去获取读锁 —这种场景就是我们之前分析的场景1啦
我们一个个的来分析哈。线程A获取了读锁且未释放,然后线程B去获取写锁。代码示例如下:
public void read() {
try {
System.out.println(Thread.currentThread().getName() + " 获取读锁");
readLock.lock();
System.out.println(Thread.currentThread().getName() + " 获取写锁");
writeLock.lock(); // **注释1**
Thread.sleep(3 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + " 释放读锁");
readLock.unlock();
}
}
在注释1的位置再次获取读锁,读锁的获取我们上面分析了,所以先看看获取写锁的代码,调用就是ReentrantReadWriteLock中WriteLock的lock方法:
public void lock() {
sync.acquire(1);
}
接着调用AQS中的acquire:
public final void acquire(int arg) {
// 去获取读锁,获取失败的话,先加入等待队列,然后判断该该线程的前置结点是否为头结点,如果是的话再次去尝试获取锁,如果不是则把该结点的状态改为Signal然后调用LockSurpport.park来阻塞该线程,等待其他线程释放锁后唤醒
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
上面的逻辑就是和之前讲的ReentrantLock的lock方法一样,只是tryAcquire的实现不一样,来看看tryAcquire的实现:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
// 获取 state 变量的值
int c = getState();
// 获取写锁的次数,写锁是 state 的第16位表示的,之前也说过
int w = exclusiveCount(c);
// c 不等于0 则表示已经有线程获取了锁且未释放
if (c != 0) { // **注释1**
// (Note: if c != 0 and w == 0 then shared count != 0)
// w == 0 表示之前没有线程获取写锁 ,current != getExclusiveOwnerThread() 表示已经获取写锁的线程不是当前正在申请获取写锁的线程;直接返回false ,获取写锁失败
if (w == 0 || current != getExclusiveOwnerThread()) // **注释2**
return false;
// 获取写锁的次数超过最大值 抛出异常
if (w + exclusiveCount(acquires) > MAX_COUNT)// **注释3**
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
// 获取写锁成功, 重置 state 的值,即 state + 1
/**
* 这里获取写锁成功的case是什么样的呢? 之前的条件是 c != 0 表示有线程获取了锁,
* 如果 w == 0 ,那么表示 有线程获取了锁且获取的都是读锁,那么 return false,获取写锁失败;
* 如果 w != 0, 如果当前线程不是独占线程,那么return false,获取写锁失败,如果当前线程就是独占线程,那么获取读锁成功;
* 什么意思呢?假设A 线程获取了写锁,然后要调用另一个方法也要获取写锁,那么是可以获取锁成功的,这就是锁重入。源代码的注释 Reentrant acquire 应该就是这个意思
*
*/
setState(c + acquires);// **注释4**
return true;
}
// 走到这里表示之前没有线程获取锁
// 是否要阻塞或者cas操作state是否成功,需要阻塞或自旋不成功直接返回false,获取写锁失败,writerShouldBlock 默认为false
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))// **注释5**
return false;
// 设置当前线程为独占线程,返回true获取写锁成功
setExclusiveOwnerThread(current);// **注释6**
return true;
}
1.线程A获取了读锁且线程未释放然后线程B去获取写锁
由之前的分析可知, A线程获取了读锁后, state值变为 65536, 即c = 65536,所以注释1成立,由于只有A线程获取了读锁,所以写锁的数量w = 0,所以注释2的w == 0成立,返回false 即获取写锁失败----这就是如果有线程获取了读锁,那么获取写锁的线程需要阻塞。
2.线程A获取了写锁且未释放再去获取读锁
实例如下:
public void write() {
try {
writeLock.lock();
System.out.println(Thread.currentThread().getName() + " 获取写锁");
// 同一个线程在获取写锁后可以再获取读锁, 锁降级;反过来,同一个线程获取读锁后是不可以再获取写锁
read(); // **获取读锁**
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + " 释放写锁");
writeLock.unlock();
}
}
来分析下:由于是第一次获取锁,所谓 c = 0,且注释5一般都会成立的,state + 1,所以设置当前线程A为独占线程。
然后线程A再去获取读锁,之前分析了获取读锁对应的代码,为了便于分析我把相应的代码再次copy下:
Thread current = Thread.currentThread();
int c = getState(); // 获取state 的值
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) // **注释1**
return -1;
// 因为 state 的高16位表示读锁的获取次数,低16位表示写锁的次数
// r 等于 state 右移 16位,相当于读锁的次数
int r = sharedCount(c);
System.out.println("r = " + r);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {// **注2**
firstReader = current;
firstReaderHoldCount = 1;
}
......
return 1;
}
由于A已经获取了写锁,所以c = 1,exclusiveCount©表示的是写锁的次数 等于1,getExclusiveOwnerThread等于线程A ,所以注释1不成立,
r:表示读线程的个数,等于0,所以注释2成立,且cas操作state的值为 1+ 65536 = 65537。
然后由于 r = 0,所以设置线程A为第一个获取读锁的线程,写第一个线程获取读锁的次数置为1。-----可以看出当一个线程获取了写锁后想要再次获取读锁是可以的,即所降级
3.线程A获取了读锁再去申请写锁
线程A 获取了读锁 ,那么state = 65536,由于state 的 高16位表示读锁的次数,即读锁的次数等于1,其他的获取读锁的代码不用分析了。
线程A再去获取写锁, 由于state = 65535 ,那么 c = 65536,w = exclusiveCount© 就是 65536 & 65535 = 0,即写锁的次数等于0,注释2 if (w == 0 || current != getExclusiveOwnerThread()) 成立 ,return false,表示获取写锁失败。–-验证了 ReentrantReadWriteLock 不可以锁升级即线程获取读锁后再去获取写锁是要加入等待队列中的。
4.线程A获取了写锁且未释放,线程B去获取读锁
线程A获取了写锁,那么state = 1,且独占线程exclusiveOwnerThread = 线程A。线程B 再去获取读锁,exclusiveCount表示写锁的次数= 1,getExclusiveOwnerThread()是线程A ,所以读锁的代码if (exclusiveCount© != 0 && getExclusiveOwnerThread() != current) 成立,return -1,获取读锁失败— 验证了 一个线程获取了写锁,其他线程获取读锁需要阻塞等待。
5. 线程A获取了写锁未释放,然后线程B也要去获取写锁
这个就不用分析了,很简单的。答案是:B线程去获取写锁是要阻塞等待的。写锁的获取流程图如下:
写锁获取失败的逻辑这里再次说下,获取写锁的代码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
如果获取写锁失败,那么会走到acquireQueued的逻辑中,先执行addWaiter,addWaiter就是加当前线程加入等待队列的尾部,具体逻辑如下:
private Node addWaiter(Node mode) {
Node node = new Node(mode);
for (;;) {
Node oldTail = tail;
// 如果oldTail不等于null,那么就把当前线程对应的结点放到等待队列的尾部
if (oldTail != null) {
// 当前结点的前置指针Node.PREV 指向 oldTail
U.putObject(node, Node.PREV, oldTail);
// tail 指针指向 当前结点
if (compareAndSetTail(oldTail, node)) {
// 之前的 tail 结点的后置指针指向 当前线程的结点
oldTail.next = node;
return node;
}
} else {// oldTail == null , 那就 new 一个空结点且把 tail 和 head 指向这个结点
initializeSyncQueue();
}
}
}
这个过程很简单,就是将当前线程对应的node插入到等待队列的最后,然后将tail指针指向它,如果对其中的cas操作不清楚的话,看我之前的文章。
但是有一点需要注意,就是头结点指向的是一个直接new出来的空结点,他的后置结点才表示正在在等待的线程结点。acquireQueued的代码如下:
final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) {
// 判断当前线程对应的node的前置结点是否是头结点,如果是头结点就去尝试获取锁;因为如果前置结点是头结点,那么表示自己有很大的机会可以获取到锁(因为获取锁的线程的任务可能随时结束,所以可以尝试看看能不能获取到锁)
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
// 如果获取到锁,那么设置当前线程的结点为头结点
setHead(node);
// 之前的头结点断开连接
p.next = null; // help GC
// 返回 false 表示不需要阻塞
return interrupted;
}
// 如果前置结点不是头结点 或者 头结点尝试获取锁失败 那么走下面的阻塞逻辑
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
shouldParkAfterFailedAcquire的主要作用是判断是否需要中断和删除已经取消的线程结点。代码如下:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 注意Node的waitStatus字段我们在上面创建Node的时候并没有指定 ,默认值是0
// waitStatus 的4种状态
//static final int CANCELLED = 1; // 取消任务
//static final int SIGNAL = -1; //等待被唤醒
//static final int CONDITION = -2; //条件锁使用
//static final int PROPAGATE = -3; //共享锁时使用
int ws = pred.waitStatus; // 前置结点的状态
/**
* 如果前置结点的状态是 SIGNAL 等待被唤醒,那么直接返回true,表示当前线程对应的结点需要被阻塞;因为前面的线程还在等待被唤醒,更轮不到你了,直接等待阻塞吧
*/
if (ws == Node.SIGNAL) //
return true;
if (ws > 0) { //
/*
*ws > 0 表示该结点对应的线程已经取消任务了,那么循环遍历删除 已取消任务的结点,这里面没啥难度,就是双向链表的删除
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 设置前置结点的 waitStatus 为SIGNAL ,等待被唤醒
*/
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
// return false 的意义:当前线程对应的结点的前置结点已经设置为SIGNAL 等待被唤醒,但是不直接返回true,如果返回true就直接要阻塞当前线程啦,返回false,那么之前 acquireQueued 方法的 无限循环 会再次判断 当前线程的前置结点是否是 head结点,如果是的话就尝试获取锁。这样做的目的就是 :Java 的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到核心态,而状态的切换是需要花费很多CPU时间的。
return false;
}
parkAndCheckInterrupt方法就比较简单,即真正实现阻塞的方法。如下:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
就是调用LockSupport.park(this)来阻塞当前线程,LockSupport的底层也是通过CAS实现的。
读线程获取失败后的处理逻辑类似,这里就不说了。下面讲读写锁的释放。
读写锁的释放 unlock
先讲读锁的释放,举例来说明啊,假设有3个线程A、B、C依次获取了读锁,然后依次释放读锁。AQS中的代码如下:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
tryReleaseShared对应于ReentrantReadWriteLock中的实现,代码如下:
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
System.out.println("tryReleaseShared :: current = "+current.getName()+", tid = "+current.getId());
// 如果当前线程是第一个获取读锁的线程
if (firstReader == current) { // **注释1**
// assert firstReaderHoldCount > 0;
// 表示第一个获取读锁的线程只获取了一次读锁,没有锁重入,那么直接将 firstReader 置 null
if (firstReaderHoldCount == 1)
firstReader = null;
else// 表示第一个获取读锁的线程重入了,获取线程的次数减1
firstReaderHoldCount--;
} else {
// 表示当前线程不是第一个获取读锁的线程,rh 赋值为 最后一个获取读锁的线程信息
HoldCounter rh = cachedHoldCounter;
System.out.println("tryReleaseShared :: rh = "+rh+" tid = " + (rh != null ? rh.tid : "[rh == null]"));
// rh 等于 null 或者 当前线程不是最后一个获取读锁的线程,从当前的线程 ThreadLocal 中获取自己的线程信息
if (rh == null || rh.tid != getThreadId(current))// **注释2**
rh = readHolds.get();
int count = rh.count;
System.out.println("tryReleaseShared :: rh = "+rh+", count = "+count);
// count <= 1 表示 该线程只获取了一次锁,释放的时候移除他对应的线程信息
if (count <= 1) {// **注释3**
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
// 线程数减1
--rh.count;
}
// 无限循环来设置 state 的值,
for (; ; ) {
int c = getState();
int nextc = c - SHARED_UNIT;// **注释4**
if (compareAndSetState(c, nextc)){ // **注释5**
System.out.println("tryReleaseShared :: nextc = "+ nextc);
return nextc == 0;
}
}
}
来实例分析:
线程A释放锁,由于线程A第一个获取读锁 , 那么注释1成立,firstReader置空,执行到注释4, nextc = c - 65536,c 等于三个线程获取读锁后的state的值,即 65536 + 65536 + 65536;所以nextc = 65535 + 65535;
注释5:重置state的值为 nextc,return false;
线程B释放锁,注释1不成立,因为此时firstReader已经置空啦,走到注释2,由于cachedHoldCounter表示的是最后一个获取读锁的线程信息,所以rh.tid != getThreadId(current)成立,通过ThreadLocal获取线程B的信息;
由于线程B只获取了一次锁,所以count <= 1成立,删除线程B在ThreadLocal中保存的信息。count–;此时nextc = 65536+ 65536 - 65536 = 65536 ,return false。
线程C释放锁,走到注释2,cachedHoldCounter保存的就是线程C的信息,所以if语句不成立,然后由于线程C也只获取了一次读锁,所以移除保存在ThreadLocal中的信息,count–。此时nectc = 65536 - 65536 = 0;retrun true。
doReleaseShared的流程如下:
private void doReleaseShared() {
for (;;) {
Node h = head;
// 表示等待队列中有等待的线程结点
if (h != null && h != tail) {
int ws = h.waitStatus;
// 等待被唤醒
if (ws == Node.SIGNAL) {
// 通过cas操作 将 头结点的 waitStatus置为0
if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h); // 唤醒头结点的 后续等待结点
}
else if (ws == 0 &&
!h.compareAndSetWaitStatus(0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
这个逻辑就是唤醒后继的在等待队列中的结点。unparkSuccessor的操作如下:
private void unparkSuccessor(Node node) {
// 此处的node表示的是 head 结点
int ws = node.waitStatus;
// 如果 头结点的 waitStatus < 0则置为0
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
// s 表示头结点的后继结点,之前 获取锁失败的时候 讲到要将该线程结点加入到等待队列的尾部,但是 如果等待队列还未初始化,则new 一个空的Node 来表示head结点,所以head结点的后续结点才是真正的等待被唤醒的线程结点。
Node s = node.next;
// 后继结点为null 或者 waitStatus > 0表示该线程的任务被取消
if (s == null || s.waitStatus > 0) {
s = null;
// 依次从后向前找 ,找到最先等待的结点,然后调用LockSupport.unpark来唤醒
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if (s != null)
LockSupport.unpark(s.thread);
}
AQS中的代码如下:
private void unparkSuccessor(Node node) {
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease在ReentrantReadWriteLock中的实现如下:
protected final boolean tryRelease(int releases) {
// 如果当前线程不是独占线程 抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases; // nextc = state -1
boolean free = exclusiveCount(nextc) == 0; // 表示写锁的次数等于0
if (free)
setExclusiveOwnerThread(null); // 如果写锁全部释放,那么 独占现在置空
setState(nextc);// state 减 1
return free;
}
如果写锁全部释放后,执行unparkSuccessor逻辑如上。
推荐阅读:
股票线是怎么画出来的?想怎么画就怎么画!
犹豫要不要用DataBinding?这篇文章帮你解惑
我的新书,《第一行代码 第3版》已出版!
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注