【原创】Java并发编程系列18 | 读写锁(下)
2021-03-15 04:30
标签:unlock stat cas 基本 理解 执行 shel res nbsp rwl.writeLock().lock()的调用 先分析一下可以获取写锁的条件: } 对于非公平锁来说,不需要关心队列中的情况,有机会直接尝试抢锁就好了,所以直接返回false。 写锁释放比较简单,跟之前的重入锁释放基本类似,看下源码: 读写锁支持锁降级。锁降级就是写锁是可以降级为读锁的,但是需要遵循获取写锁、获取读锁、释放写锁的次序。 支持降级锁的情况:线程A持有写锁时,线程A要读取共享数据,线程A直接获取读锁读取数据就好了。 tryAcquireShared()方法中,当前线程占用写锁时是可以获取读锁的,如下: 不支持锁升级 持有写锁的线程,去获取读锁的过程称为锁降级;持有读锁的线程,在没释放的情况下不能去获取写锁的过程称为锁升级。 读写锁多用于解决读多写少的问题,最典型的就是缓存问题。如下是官方给出的应用示例: 可以获取写锁的情况只有两种: 【原创】01|开篇获奖感言 之前,给大家发过三份Java面试宝典,这次新增了一份,目前总共是四份面试宝典,相信在跳槽前一个月按照面试宝典准备准备,基本没大问题。 看到这里,证明有所收获 【原创】Java并发编程系列18 | 读写锁(下) 标签:unlock stat cas 基本 理解 执行 shel res nbsp 原文地址:https://blog.51cto.com/15009303/2552735
收录于话题
#进阶架构师 | 并发编程专题
12个
本文为何适原创并发编程系列第 18 篇,文末有本系列文章汇总。
通过以下几部分来分析Java提供的读写锁ReentrantReadWriteLock:
为什么需要读写锁
读写锁的使用Demo
ReentrantReadWriteLock类结构
记录读写锁状态
源码分析读锁的获取与释放
源码分析写锁的获取与释放
锁降级
读写锁应用
本文涉及到上下文联系较多,经常需要上下滑动查看,篇幅太多很不方便,而且文章太长阅读体验也不好,所以分成读写锁(上)和读写锁(下)两篇。上篇为【原创】Java并发编程系列17 | 读写锁八讲(上),没看过的可以先看看。本文是下篇,从“源码分析写锁的获取与释放”开始。7. 写锁获取
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 写锁实现了获取锁的方法,下文详细讲解
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 获取锁失败进入同步队列,等待被唤醒,AQS一文中重点讲过
selfInterrupt();
}
当前锁的状态
1)没有线程占用锁(读写锁都没被占用) 2)线程占用写锁时,线程再次来获取写锁,也就是重入
AQS队列中的情况,如果是公平锁,同步队列中有线程等锁时,当前线程是不可以先获取锁的,必须到队列中排队。
写锁的标志位只有16位,最多重入2^16-1次。/**
* ReentrantReadWriteLock.Sync.tryAcquire(int)
*/
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);// 写锁标志位
// 进到这个if里,c!=0表示有线程占用锁
// 当有线程占用锁时,只有一种情况是可以获取写锁的,那就是写锁重入
if (c != 0) {
/*
* 两种情况返回false
* 1.(c != 0 & w == 0)
* c!=0表示标志位!=0,w==0表示写锁标志位==0,总的标志位不为0而写锁标志位(低16位)为0,只能是读锁标志位(高16位)不为0
* 也就是有线程占用读锁,此时不能获取写锁,返回false
*
* 2.(c != 0 & w != 0 & current != getExclusiveOwnerThread())
* c != 0 & w != 0 表示写锁标志位不为0,有线程占用写锁
* current != getExclusiveOwnerThread() 占用写锁的线程不是当前线程
* 不能获取写锁,返回false
*/
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 重入次数不能超过2^16-1
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
/*
* 修改标志位
* 这里修改标志位为什么没有用CAS原子操作呢?
* 因为到这里肯定是写锁重入了,写锁是独占锁,不会有其他线程来捣乱。
*/
setState(c + acquires);
return true;
}
/*
* 到这里表示锁是没有被线程占用的,因为锁被线程占用的情况在上个if里处理并返回了
* 所以这里直接检查AQS队列情况,没问题的话CAS修改标志位获取锁
*/
if (writerShouldBlock() || // 检查AQS队列中的情况,看是当前线程是否可以获取写锁
!compareAndSetState(c, c + acquires)) // 修改写锁标志位
return false;
setExclusiveOwnerThread(current);// 获取写锁成功,将AQS.exclusiveOwnerThread置为当前线程
return true;
简单看下writerShouldBlock()
writerShouldBlock():检查AQS队列中的情况,看是当前线程是否可以获取写锁,返回false表示可以获取写锁。
对于公平锁来说,如果队列中还有线程在等锁,就不允许新来的线程获得锁,必须进入队列排队。
hasQueuedPredecessors()方法在重入锁的文章中分析过,判断同步队列中是否还有等锁的线程,如果有其他线程等锁,返回true当前线程不能获取读锁。
// 公平锁
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
// 非公平锁
final boolean writerShouldBlock() {
return false;
}
8. 写锁释放
public void unlock() {
sync.release(1);
}
/**
* 释放写锁,如果释放之后没有线程占用写锁,唤醒队列中的线程来获取锁
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);// 唤醒head的后继节点去获取锁
return true;
}
return false;
}
/**
* 释放写锁,修改写锁标志位和exclusiveOwnerThread
* 如果这个写锁释放之后,没有线程占用写锁了,返回true
*/
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;
}
9. 锁降级
为什么要支持锁降级?
如果不支持锁降级会怎么样?
线程A持有写锁时,线程A要读取共享数据,但是线程A不能获取读锁,只能等待释放写锁。
当线程A释放写锁之后,线程A获取读锁要和其他线程抢锁,如果另一个线程B抢到了写锁,对数据进行了修改,那么线程B释放写锁之后,线程A才能获取读锁。线程B获取到读锁之后读取的数据就不是线程A修改的数据了,也就是脏数据。
源码中哪里支持锁降级?protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
/*
* 根据锁的状态判断可以获取读锁的情况:
* 1. 读锁写锁都没有被占用
* 2. 只有读锁被占用
* 3. 写锁被自己线程占用
* 总结一下,只有在其它线程持有写锁时,不能获取读锁,其它情况都可以去获取。
*/
if (exclusiveCount(c) != 0 && // 写锁被占用
getExclusiveOwnerThread() != current) // 持有写锁的不是当前线程
return -1;
...
读写锁是不支持锁升级的。获取写锁的tryAcquire()方法:protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
/*
* (c != 0 & w == 0)时返回false,不能获取写锁
* c != 0 表示state不是0
* w == 0 表示写锁标志位state的低16位为0
* 所以state的高16位不为0,也就是有线程占有读锁
* 也就是说只要有线程占有读锁返回false,不能获取写锁,当然线程自己持有读锁时也就不能获取写锁了
*/
if (c != 0) {
if (w == 0 || current != getExclusiveOwnerThread())
return false;
...
8. 应用
class CachedData {
Object data;
volatile boolean cacheValid;
// 读写锁实例
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
// 获取读锁
rwl.readLock().lock();
if (!cacheValid) { // 如果缓存过期了,或者为 null
// 释放掉读锁,然后获取写锁 (后面会看到,没释放掉读锁就获取写锁,会发生死锁情况)
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
if (!cacheValid) { // 重新判断,因为在等待写锁的过程中,可能前面有其他写线程执行过了
data = ...
cacheValid = true;
}
// 获取读锁 (持有写锁的情况下,是允许获取读锁的,称为 “锁降级”,反之不行。)
rwl.readLock().lock();
} finally {
// 释放写锁,此时还剩一个读锁
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
use(data);
} finally {
// 释放读锁
rwl.readLock().unlock();
}
}
}
总结
读锁和写锁都没有线程占用
当前线程占用写锁,也就写锁重入
读写锁支持锁降级,不支持锁升级。锁降级就是写锁是可以降级为读锁的,但是需要遵循获取写锁、获取读锁、释放写锁的次序。
读写锁多用于解决读多写少的问题,最典型的就是缓存问题。
参考资料
《Java 并发编程之美》
《Java 并发编程实战》
《Java 并发编程的艺术》并发系列文章汇总
【原创】02|并发编程三大核心问题
【原创】03|重排序-可见性和有序性问题根源
【原创】04|Java 内存模型详解
【原创】05|深入理解 volatile
【原创】06|你不知道的 final
【原创】07|synchronized 原理
【原创】08|synchronized 锁优化
【原创】09|基础干货
【原创】10|线程状态
【原创】11|线程调度
【原创】13|LockSupport
【原创】14|AQS 源码分析
【原创】15|重入锁 ReentrantLock
【原创】16|公平锁与非公平锁
【原创】Java并发编程系列17 | 读写锁八讲(上)
《java面试宝典5.0》(初中级)
《350道Java面试题:整理自100+公司》(中高级)
《资深java面试宝典-视频版》(资深)
《Java[BAT]面试必备》(资深)
分别适用于初中级,中高级,资深级工程师的面试复习。
内容包含java基础、javaweb、mysql性能优化、JVM、锁、百万并发、消息队列,高性能缓存、反射、Spring全家桶原理、微服务、Zookeeper、数据结构、限流熔断降级等等。
获取方式:点“在看”,V信关注上述单号并回复 【面试】即可领取,更多精彩陆续奉上。
必须点个在看支持呀,喵