面试:谈谈你对ReentrantReadWriteLock的理解,推荐看看(图解版)
转自:Ccww技术博客
前言
在前面看了ReentrantLock面试分析,在面试问了多并发的就少不了ReentrantReadWriteLock读写锁了,那具体是什么问题?我们来看看:
ReentrantReadWriteLock读写锁是什么,有什么用?ReentrantReadWriteLock具有哪些特性?ReentrantReadWriteLock是怎么实现读写锁的获取?ReentrantReadWriteLock读写锁的释放
ReentrantReadWriteLock读写锁是什么,有什么用?
平时存在很多场景,大部分线程提供读服务,只有少数的写服务。然而读服务不存在数据竞争问题,如果一个线程在读时禁止其他线程读势必会导致性能降低,因此延伸了ReadWriteLock读写锁。
ReentrantReadWriteLock读写锁维护着一对锁,一个读锁和一个写锁。
通过分离读锁和写锁,使得对共享资源的访问分为读访问和写访问,多个线程可以同时对共享资源进行读访问,读写锁可以极大地提高并发量。但是同一时间只能有一个线程对共享资源进行写访问,其他所有读线程和写线程都会被阻塞。
ReentrantReadWriteLock具有哪些特性?
读写锁的主要特性:
公平性:支持公平性和非公平性。
重入性:支持重入。读写锁最多支持65535个递归写入锁和65535个递归读取锁。
锁降级:遵循获取写锁、获取读锁在释放写锁的次序,写锁能够降级成为读锁
读写锁除了读读不互斥,读写、写读、写写都是互斥的
是否互斥 读 写 读 否 是 写 是 是
在初步了解了ReentrantReadWriteLock读写锁的原理的情况,我们来讲讲其到底是怎么实现读写锁的?
ReentrantReadWriteLock是怎么实现读写锁的获取?
ReentrantReadWriteLock实现读写锁以及公平性是依赖:
// 读锁public static class ReadLock implements Lock, java.io.Serializable {}// 写锁public static class WriteLock implements Lock, java.io.Serializable {}// 同步器-公平性abstract static class Sync extends AbstractQueuedSynchronizer {}
接下来我们通过构造函数创建一个非公平性的ReentrantReadWriteLock读写锁的来看看其实现原理:
// 默认构造方法*public ReentrantReadWriteLock() {this(false);}public ReentrantReadWriteLock(boolean fair) {//默认非公平锁sync = fair ? new FairSync() : new NonfairSync();//创建读锁readerLock = new ReadLock(this);//创建写锁writerLock = new WriteLock(this);}
读写锁ReadLock和WriteLock其都是使用同一个Sync,
//读锁上锁protected ReadLock(ReentrantReadWriteLock lock) {sync = lock.sync;}//写锁上锁protected WriteLock(ReentrantReadWriteLock lock) {sync = lock.sync;}
那我们来看看这个sync有什么奇特之处?
在Sync中32位的同步状态state,划分成2部分,高16位表示读,低16位表示写,假设当前同步状态为s:
读状态为 s >>> 16,即无符号补0右移16位,读状态增加1,等于 s + (1 << 16)
写状态为 s & 0000FFFF,即将高16位全部抹去,写状态增加1,等于 s + 1

abstract static class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID = 6317671515068378041L;static final int SHARED_SHIFT = 16;//读状态加+1使用变量static final int SHARED_UNIT = (1 << SHARED_SHIFT);static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;/** 获取读状态 */static int sharedCount(int c) { return c >>> SHARED_SHIFT; }/** 获取写状态 */static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }...}
readLock::lock()实现
调用readLock.lock()进行获取读锁共享锁:
public void lock() {sync.acquireShared(1);}public final void acquireShared(int arg) {//尝试获取同步状态if (tryAcquireShared(arg) < 0)//获取失败则加入到同步队列doAcquireShared(arg);}
尝试获取同步状态tryAcquireShared(arg):
互斥锁-写状态不为0(写锁已被获取),且获取写锁的不是当前线程,那么直接返回失败(已写锁状态-->进入等待队列
判断
readerShouldBlock()同步队列中第一个节点是否为写线程的节点,如果是写线程需要阻塞,如果是读线程不应该阻塞,需要更新读状态成功如果不存在,设置到当前线程在最近获取读锁的线程
CachedHoldCounter,且重入次数加+1存在,重入次数加+1
如果之前还没有线程获取读锁,记录第一个读者为当前线程(无锁状态时-->获取读锁)
如果有线程获取了读锁且是当前线程是队列中第一个读线程,则把其重入次数加1(已有读锁时-->线程读锁再重入)
如果有线程获取了读锁且当前线程不是队列中第一个读线程,先判断
readHolds是否存在当前线程(已有读锁时-->当前线程非首个获取读锁)
protected final int tryAcquireShared(int unused) {//获取当前线程Thread current = Thread.currentThread();//获取同步状态int c = getState();//如果写状态不为0(写锁已被获取),且获取写锁的不是当前线程,那么直接返回失败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 {//如果有线程获取了读锁当前线程不是队列中第一个读线程HoldCounter rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current))cachedHoldCounter = rh = readHolds.get();else if (rh.count == 0)readHolds.set(rh);rh.count++;}return 1;}//尝试获取读状态return fullTryAcquireShared(current);}
CAS更新读状态失败,或者readerShouldBlock()判断出第一个节点为写线程的节点,调用此方法尝试获取读状态
final int fullTryAcquireShared(Thread current) {HoldCounter rh = null;for (;;) {//获取同步状态int c = getState();//如果写线程已经获取到锁,且获取到锁的线程不是当前线程,那么直接返回获取失败if (exclusiveCount(c) != 0) {if (getExclusiveOwnerThread() != current)return -1;//如果当前线程已经获取了写锁,现在正在尝试获取读锁,//那么此线程不应该给同步队列中的写线程让位(让位是为了防止写线程饥饿),//因为会造成死锁。} else if (readerShouldBlock()) {...}if (sharedCount(c) == MAX_COUNT)throw new Error("Maximum lock count exceeded");//不给同步队列中的写线程让位,而是直接尝试获取读锁if (compareAndSetState(c, c + SHARED_UNIT)) {...}}}
获取读锁失败了就可能要排队,其原理跟ReentrantLock差不多。其中setHeadAndPropagate()唤醒下一个读节点,是一个唤醒一个唤醒队列中的读锁,而不是一次性唤醒后面所有的读节点。
这样不是很简洁明了,那直接看看图:
当已写锁状态-->进入等待队列

当为无锁状态时-->获取读锁

当已有读锁时-->读锁再重入

当已有读锁时-->当前线程非首个获取读锁

还有其他复杂的情况,可以自己熟悉后再慢慢理解,现在看完了readLock::lock()实现,我们来研究研究writeLock::lock()实现,你会发现比较简单。
writeLock::lock()实现
获取写锁
public final void acquire(int arg) {//尝试获取同步状态,失败则加入到同步队列中if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}
尝试获取同步状态
获取同步状态
state,是否为0,是否已经获取过锁了写状态-互斥
exclusiveCount(c)==0写状态为0(说明读状态不为0,已获取读锁)或者当前线程current != getExclusiveOwnerThread()(写锁已经被抢占),都不允许再次获取其他线程写锁当前线程已经获取过写锁,这里是重入了,直接把
state加1;state不为0时(说明已获取读/写锁了)state为0时(说明还没有读/写锁了),就尝试更新state的值(非公平模式writerShouldBlock()返回false),该线程设置为占有者
protected final boolean tryAcquire(int acquires) {//获取当前线程Thread current = Thread.currentThread();//获取同步状态int c = getState();//获取写状态-互斥int w = exclusiveCount(c);//已获取锁了(读/写)if (c != 0) {// 如果同步状态不为0,且写状态为0,那么说明if (w == 0 || current != getExclusiveOwnerThread())//如果读状态不为0,或者写锁已经被抢占,那么返回获取写锁失败return false;if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");//当前线程已经获取过写锁,这里是重入了,直接把state加1即可setState(c + acquires);return true;}//还没有锁(读/写),更新同步状态且设置该线程为占有者if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))return false;setExclusiveOwnerThread(current);return true;}
失败处理流程跟ReadLock模式一样。也来具体看看流程图示:
当为无锁状态-->获取写锁

当已有写锁-->获取重入写锁

当已有写锁-->不同线程获取写锁

那我们来总结一下获取锁的
小结-获取锁
在获取读锁时:
如果写锁已经被其他线程获取,那么此线程将会加入到同步队列,挂起等待唤醒。
如果写锁没有被其他线程获取,但是同步队列的第一个节点是写线程的节点,那么此线程让位给写线程,挂起等待唤醒
如果获取读锁的线程,已经持有了写锁,那么即使同步队列的第一个节点是写线程的节点,它也不会让位给同步队列中的写线程,而是自旋去获取读锁。因为此时让位会造成死锁
获取写锁时:
如果读锁已经被获取,那么不允许获取写锁。将此线程加入到同步队列,挂起等待唤醒
如果读锁没有被获取,但是写锁已经被其他线程抢占,那么还是将此线程加入到同步队列,挂起等待唤醒
如果写锁已经被此线程持有,那么重入,即写状态+1
如果读锁和写锁都没有被获取,那么CAS尝试获取写锁
获取读写锁的研究完,我们也需要看看锁的释放
ReentrantReadWriteLock读写锁的释放
readLock.unlock()
读锁释放步骤
释放锁
如果第一个读线程是当前线程,将重入的次数减1,当减到0了就把第一个读者置为空
如果第一个读线程不是当前线程,也需要将重入的次数减1,
共享锁获取的次数减1, 如果减为0了说明完全释放了,才返回true
释放成功,唤醒下一个节点
// java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock.unlockpublic void unlock() {sync.releaseShared(1);}// java.util.concurrent.locks.AbstractQueuedSynchronizer.releaseSharedpublic final boolean releaseShared(int arg) {// 尝试释放锁if (tryReleaseShared(arg)) {// 唤醒下一个节点doReleaseShared();return true;}return false;}// java.util.concurrent.locks.ReentrantReadWriteLock.Sync.tryReleaseSharedprotected final boolean tryReleaseShared(int unused) {Thread current = Thread.currentThread();if (firstReader == current) {// 第一个读线程是当前线程if (firstReaderHoldCount == 1)firstReader = null;elsefirstReaderHoldCount--;} else {// 如果第一个读者不是当前线程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;}for (;;) {// 共享锁获取的次数减1int c = getState();int nextc = c - SHARED_UNIT;if (compareAndSetState(c, nextc))return nextc == 0;}}private void doReleaseShared() {for (;;) {Node h = head;if (h != null && h != tail) {int ws = h.waitStatus;// 如果头节点状态为SIGNAL,说明要唤醒下一个节点if (ws == Node.SIGNAL) {if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))continue; // loop to recheck cases// 唤醒下一个节点unparkSuccessor(h);}else if (ws == 0 &&// 把头节点的状态改为PROPAGATE成功才会跳到下面的if!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))continue; // loop on failed CAS}// 如果唤醒后head没变,则跳出循环if (h == head) // loop if head changedbreak;}}
WriteLock.unlock()
写锁释放步骤:
释放锁
state状态变量的值减1,是否完全释放锁,如果完全释放锁则设置独占为null,设置
state状态变量的值释放成功,唤醒下一个节点
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);return true;}return false;}// java.util.concurrent.locks.ReentrantReadWriteLock.Sync.tryRelease()protected final boolean tryRelease(int releases) {// 如果写锁不是当前线程占有着,抛出异常if (!isHeldExclusively())throw new IllegalMonitorStateException();// 状态变量的值减1int nextc = getState() - releases;// 是否完全释放锁boolean free = exclusiveCount(nextc) == 0;if (free)setExclusiveOwnerThread(null);// 设置状态变量的值setState(nextc);// 如果完全释放了写锁,返回truereturn free;
总结
ReentrantReadWriteLock在使用中也会,比如并发的读多写少情景,本地缓存也可以使用其实现,有时间会写一篇关于本地缓存设计方面的文章。
后台回复 学习资料 领取学习视频
如有收获,点个在看,诚挚感谢
