本文转载自【微信公众号:java进阶架构师,ID:java_jiagoushi】经微信公众号授权转载,如需转载与原文作者联系
本文为何适原创并发编程系列第 18 篇,文末有本系列文章汇总。
通过以下几部分来分析Java提供的读写锁ReentrantReadWriteLock:
为什么需要读写锁读写锁的使用DemoReentrantReadWriteLock类结构记录读写锁状态源码分析读锁的获取与释放源码分析写锁的获取与释放锁降级读写锁应用本文涉及到上下文联系较多,经常需要上下滑动查看,篇幅太多很不方便,而且文章太长阅读体验也不好,所以分成读写锁(上)和读写锁(下)两篇。上篇为【原创】Java并发编程系列17 | 读写锁八讲(上),没看过的可以先看看。本文是下篇,从“源码分析写锁的获取与释放”开始。
7. 写锁获取
rwl.writeLock().lock()
的调用
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();}
对于非公平锁来说,不需要关心队列中的情况,有机会直接尝试抢锁就好了,所以直接返回false。
// 非公平锁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要读取共享数据,但是线程A不能获取读锁,只能等待释放写锁。
当线程A释放写锁之后,线程A获取读锁要和其他线程抢锁,如果另一个线程B抢到了写锁,对数据进行了修改,那么线程B释放写锁之后,线程A才能获取读锁。线程B获取到读锁之后读取的数据就不是线程A修改的数据了,也就是脏数据。
源码中哪里支持锁降级?
tryAcquireShared()方法中,当前线程占用写锁时是可以获取读锁的,如下:
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(); } }}
总结
可以获取写锁的情况只有两种:
读锁和写锁都没有线程占用当前线程占用写锁,也就写锁重入读写锁支持锁降级,不支持锁升级。锁降级就是写锁是可以降级为读锁的,但是需要遵循获取写锁、获取读锁、释放写锁的次序。
读写锁多用于解决读多写少的问题,最典型的就是缓存问题。