目录
前言
正文
ReentrantLock和ReentrantReadWriteLock的区别
ReentrantReadWriteLock的源码解读
ReentrantReadWriteLock内部结构解读
读锁上锁的实现解读
读锁释放锁的实现解读
写锁上锁实现解读
写锁释放实现解读
源码结论总结(方便面试)
总结
课程推荐
目前也是金三银四跳槽找工作的好时机。小伙伴们可能在面试的时候被面试官问到很多关于多线程方面一系列的连锁问题。比如你有了解Java的多线程吗?你对juc并发包下的技术有了解多少?有了解过ReentrantReadWriteLock读写锁是吧,那你项目中有使用过吗?读写锁和ReentrantLock锁的区别有哪些?读写锁他的底层源码你有了解多少呢?等等一系列连锁问题。所以特意带来一篇关于ReentrantReadWriteLock读写锁的源码解读,帮助大家顺利通过面试!
ReentrantLock是所有操作都要上锁,所以锁的力度比较大
ReentrantReadWriteLock所有操作上锁的同时可以做到读读操作并发,读写互斥,写写互斥。
所以两者的区别可以说是在读多写少的情况下一定要使用到ReentrantReadWriteLock,可以提高代码的执行效率。
从这里我们能明白ReentrantReadWriteLock内部是维护了一个AQS,而AQS又分为公平和非公平,然后维护了一个ReadLock和WriteLock,两者公用一个AQS对象。但是读和写的锁具体的实现肯定是不一样的,因为要达到读写互斥、写写互斥、读读并发。接下来看读锁和写锁的具体实现。
AQS的解释:
AQS算是对于juc包下的技术的一个技术支持,相当于一个框架。
AQS内部维护了一个state状态位,和一个队列(双线链表),还有一个当前线程的引用。内部多线程并发的原子性操作使用cas自旋锁的机制来保证原子性。
state状态位:用来标识是否还占有锁,内部使用cas自旋锁来保证原子性。
队列:内部使用双向链表来存储被park()的线程。
protected final int tryAcquireShared(int unused) {
// 获取到当前线程
Thread current = Thread.currentThread();
// 获取当当前state标识位的值
int c = getState();
// exclusiveCount()方法判断当前state的值是否为写锁在做事
// getExclusiveOwnerThread() != current是判断是否是写操作里面重入了读操作
// 如果当前是写锁就直接返回-1,如果当前写锁并且当前线程跟正在执行的写线程不是同一线程就直接返回-1,并且外面的判断是返回值是否小于0
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 能执行到这里,证明当前读锁可以执行或者并发执行。
// sharedCount()方法是获取到当前线程的读锁的计数,比如第一次是0,第二次是1,不过需要连续的读锁
int r = sharedCount(c);
// readerShouldBlock()这个方法是来控制公平锁和非公平锁
// r < MAX_COUNT不能大于总共的线程数
// compareAndSetState通过cas操作来
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 赋值第一个读锁
// 注意这里的第一个并不是整个程序的第一个,是指连续的一段读锁中的第一个读锁
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
// 代表第一个读锁发生了锁重入
} else if (firstReader == current) {
firstReaderHoldCount++;
// 不是第一个获取到读锁,并且也不是第一个锁重入就会进入到下面的else代码块中
// 并且能进到这里代表是读并发过程,所以下面的操作肯定是为了并发的操作做一些缓存和记录并发总线程数量。
} 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++;
}
// 记住进入方法的判断是判断当前方法的返回值如果小于0就执行doAcquireShared()方法
// 如果大于0就执行业务逻辑代码。
return 1;
}
// 能进到这里就代表最上面的那个if中三个条件如果有一个条件没有满足
// 并且此方法也就是当前方法的一个满配版本,和当前方法很多逻辑有着冗余
// 其次最有可能进入到此方法的可能就是并发读得过程中大家通过cas来改变state状态那些没有抢到线程返回false的线程就进入到这个方法中。
// 再其次就是公平和非公平的认定也会进入到这个方法中再去处理。
return fullTryAcquireShared(current);
}
基本上每行都有注释,但是我更希望小伙伴们自己debug追一追,自己好好的思考。
这里我还是简单的概述一下读锁上锁的过程。
首先先判断当前的state状态位是否是写锁,如果是写锁的话直接返回-1,进入到之后的逻辑。如果是没有发生任何锁或者是读锁的话就继续往下执行,后面的逻辑也就是通过cas操作改变state状态位的值为读锁的一个标识。其次对于state标识符来说读锁和写锁的标识符是有区别的,也是通过这个区别来区别是读锁还是写锁。再往后走就是记录读写锁的次数和缓存了。
前面也只是看的是加锁成功都是返回1,那么我们看到返回-1执行的逻辑把
// 读锁添加失败就进入到这里
private void doAcquireShared(int arg) {
// 往addWaiter()方法添加一个共享节点
// addWaiter()方法中如果是首次进会进行一个初始化,如果不是第一个进就将共享节点添加到队列的尾部,并且返回这个共享节点
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
// 开始自旋
for (;;) {
// 获取到当前共享节点的前一个节点
final Node p = node.predecessor();
// 如果当前共享节点的前一个节点是head节点,也就是sentinel哨兵节点,就会尝试获取到读锁
if (p == head) {
// 再次尝试获取到读锁,还记得返回值吗?
// 成功是1,不成功是-1,也影响着下面的判断。
int r = tryAcquireShared(arg);
// 如果再次尝试获取读锁成功就执行下面代码,失败就往下走。
if (r >= 0) {
// 这里是尝试获取成功,将head节点指向当前节点,意思就是当前节点当sentinel哨兵节点了
// 并且对于共享节点还有一个任务就是唤醒后面的共享节点,所以当前节点后面如果是读锁也会被唤醒,所以也就是读读并发。
setHeadAndPropagate(node, r);
// 将之前的head节点,也就是sentinel哨兵节点的next指向null,所以此节点没有任何引用了,就被回gc回收
p.next = null; // help GC
// 判断是否是被打断醒来的。
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 到这里就代表前面的尝试获取读锁失败了
// shouldParkAfterFailedAcquire()方法将当前节点的前一个节点的waitState标志位设为-1
// 但是设置的过程还是自旋一次尝试获取锁
// parkAndCheckInterrupt()方法是将当前线程park休眠。
// 当前线程park之前,总共是经历过3次尝试获取读锁。
// 注意等unLock()方法执行了就会从这里唤醒线程。所以还是在自旋中。醒来又会去尝试获取锁
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
addWaiter()方法的初始化和非初始化图如下:
初始化
非初始化(新节点加入)
protected final boolean tryReleaseShared(int unused) {
// 获取到当前线程
Thread current = Thread.currentThread();
// 判断当前线程是否是第一个读锁线程,因为我们在加读锁的过程中,只对第一个读锁做了处理
// 其他的就是ThreadLocal和HoldCounter来做记录和缓存。
if (firstReader == current) {
// 如果是第一个读锁的情况下,要判断是否存在锁重入情况,firstReaderHoldCount默认是为1
if (firstReaderHoldCount == 1)
// 没有锁重入将第一个读线程置为null,就代表第一个线程已经释放完毕。
// 但是这只是缓存和计数层面,真正的释放是要改变state状态位,所以后面的for循环中改变
firstReader = null;
// 存在锁重入
else
firstReaderHoldCount--;
} else {
// 能进到else里面就代表读锁是并发了,因为不止一个线程,下面的操作也就是计数器和缓存的一些操作
// 但是真正释放还是以state状态位来决定。
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;
}
// 自旋来改变state标志位。
for (;;) {
// 获取到当前的状态值
int c = getState();
// 减去一次的读锁值
int nextc = c - SHARED_UNIT;
// cas来原子修改,这里不怕失败,因为自旋
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.
// 判断减去后的值是否为0,如果为0就代表读操作全部完成,如果不是0就代表还有线程在执行。
return nextc == 0;
}
}
这里就是释放锁的过程,整体来说就是计数器和缓存一个处理和state状态值的一个修改,如果state值为0就代表读操作全部执行完毕,可以唤醒队列中等待的线程了。
// 能进来这里代表读锁全部释放完毕
private void doReleaseShared() {
// 开始自旋
for (;;) {
// 获取到头节点
Node h = head;
// 判断队列中是否有值
if (h != null && h != tail) {
// 能进来就代表队列中有线程在等待
// 获取到head节点也就是sentinel哨兵节点的waitStatus状态位
int ws = h.waitStatus;
// 如果为-1,就代表要唤醒head.next节点
if (ws == Node.SIGNAL) {
// cas自旋来修改,如果修改失败就继续自旋修改
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// cas修改成功就唤醒head.next节点。
unparkSuccessor(h);
}
// 这里存在的意义是为了解决一个bug,但是我也不懂....
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 如果还是相等就代表队列中没有值。
if (h == head) // loop if head changed
break;
}
}
这里也就是判断队列中是否存在节点,也就是是否有写线程在休眠,然后unpark操作唤醒。
protected final boolean tryAcquire(int acquires) {
// 获取到当前线程
Thread current = Thread.currentThread();
// 获取到state状态值
int c = getState();
// 通过state值获取到排斥线程的计数值
int w = exclusiveCount(c);
// 这里要注意c!=0 可能是写锁也有可能是读锁,这里自己体会
if (c != 0) {
// c!=0 and w==0就代表当前还是读线程,并且也不是写锁的一个重入就直接返回false。
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 这里是发生了一个写的锁重入操作,并且判断是否重入到最大次数
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 因为写写和读写是互斥,所以state只为写锁来工作,所以只有重入才会累加state的值
setState(c + acquires);
// 返回true去执行重入的业务逻辑
return true;
}
// writerShouldBlock()也是控制是否公平锁
// 执行到这里能证明当前不是锁重入,而且c为0,也就是当前没有任何读写操作,
// 然后通过cas操作来竞争
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
// AQS中维护的互斥线程指向竞争到的线程
setExclusiveOwnerThread(current);
// 返回true然后执行写线程中的逻辑
return true;
}
写锁还是比读锁容易很多,有ReentrantLock的底子的小伙伴看起来是非常的轻松的。
这里也就是判断是否当前state状态位是读线程还是当前线程的一个锁重入,如果都不是就通过cas来竞争到写锁。所以state状态位为读或者是cas没竞争到写锁的线程都会返回false。我们往下看
addWaiter()就是初始化队列或者是添加一个互斥节点到队列尾部。所以直接看到acquireQueded方法。
// 能进到这个方法,就代表没竞争到写锁,或者当前是读锁
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 自旋开始
for (;;) {
// 获取到当前节点的prev节点,也就是当前节点的上一个节点
final Node p = node.predecessor();
// 因为在进入acquireQueued()方法之前就通过addWaiter()方法进行初始化或者是添加互斥节点
// 所以这里判断当前节点的上一个节点是否是head节点,如果是就再一次尝试获取到写锁
// 因为head节点的next节点就是要队列中第一个线程节点。因为真正的一个节点是sentinel节点
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 走到这里就代表当前节点的上一个节点不是head节点或者是尝试获取到写锁失败了
// shouldParkAfterFailedAcquire()方法是将当前节点的上一个节点的waitStatus状态改成-1
// 修改的过程还会自旋一次来尝试获取写锁,如果还是失败就会进入到parkAndCheckInterrupt()方法进行当前线程的休眠。
// 这里注意等唤醒后,接着这里执行还是会继续自旋通过tryAcquire()尝试获取写锁。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这里比较简单,也就是判断当前节点的上一个节点是否是head节点,如果是的话就代表当前节点的线程就是队列的第一个线程(真正的第一个还是sentinel哨兵节点)。如果是head节点就会再去尝试获取锁,如果再尝试获取锁失败就会把当前节点的上一个节点的waitStatus状态位改成-1,然后当前节点进入到park休眠,等待被唤醒。唤醒后再通过自旋尝试获取写锁。
protected final boolean tryRelease(int releases) {
// 判断当前线程是否是AQS内部的一个当前执行线程的指向线程,不是的话就直接抛出异常,就是文不对题
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 因为可能存在锁重入,所以要获取到state值来减去1
int nextc = getState() - releases;
// 判断state值减去1后是否还有互斥计数,如果没有就返回true
boolean free = exclusiveCount(nextc) == 0;
// 返回为true就将AQS内部正在执行的互斥线程的指向为null,如果不为true,就代表还有写锁重入
if (free)
setExclusiveOwnerThread(null);
// 修改state的值
setState(nextc);
// 如果还要锁重入就返回false,相反就返回true
return free;
}
这里就是一个state状态值的一个修改,因为释放是减去1,但是可能存在锁重入。
1.ReadLock和WriteLock共用一个AQS对象,那么他们的state状态标志位是如何识别当前是读锁还是写锁?
2.不管是读锁还是写锁他们一共经历过几次尝试获取锁才进入到park操作?
大家可以评论区给出自己的答案,并且也可以出一些关于读写锁的题目。
大家一起互帮互助实现共赢~
写锁的逻辑还是比较容易,但是对于读锁还是比较复杂,因为要实现一个读读并发。
也没啥好总结的,因为人与人之间的理解是不同的,并且这么复杂的源码用文字来描述清楚肯定是很困难的,所以有什么问题欢迎在评论区讨论!
免费课程,绝对良心推荐
配合图解很好理解,但是讲的不深,还是希望大家自己debug来理解https://www.bilibili.com/video/BV16J411h7Rd?p=253
最后本帖如果对大家有帮助,希望大家点赞+关注,一直在努力的更新各种框架和技术的源码解读