面试:谈谈你对ReentrantReadWriteLock的理解,推荐看看(图解版)
共 7351字,需浏览 15分钟
·
2020-10-23 22:17
转自: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.unlock
public void unlock() {
sync.releaseShared(1);
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer.releaseShared
public final boolean releaseShared(int arg) {
// 尝试释放锁
if (tryReleaseShared(arg)) {
// 唤醒下一个节点
doReleaseShared();
return true;
}
return false;
}
// java.util.concurrent.locks.ReentrantReadWriteLock.Sync.tryReleaseShared
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// 第一个读线程是当前线程
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} 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 (;;) {
// 共享锁获取的次数减1
int 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 changed
break;
}
}
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();
// 状态变量的值减1
int nextc = getState() - releases;
// 是否完全释放锁
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
// 设置状态变量的值
setState(nextc);
// 如果完全释放了写锁,返回true
return free;
总结
ReentrantReadWriteLock在使用中也会,比如并发的读多写少情景,本地缓存也可以使用其实现,有时间会写一篇关于本地缓存设计方面的文章。
后台回复 学习资料 领取学习视频
如有收获,点个在看,诚挚感谢