AQS源码解读(七)——ReentrantReadWriteLock原理详解(读写锁是一把锁吗?如何一把锁两个状态?)

readerLock = new ReadLock(this);

writerLock = new WriteLock(this);

}

ReentrantReadWriteLock中的NonfairSyncFairSync也都继承自ReentrantReadWriteLock#Sync,但是并没有像ReentrantLock中一样,分别实现获取锁的逻辑,而是分别实现了两种阻塞的策略,writerShouldBlockreaderShouldBlock。获取锁模板方法已经在ReentrantReadWriteLock#Sync中实现了。

1、NonfairSync

  • NonfairSync#writerShouldBlock :写线程在抢锁之前永远不会阻塞,非公平性。

  • NonfairSync#readerShouldBlock:读线程抢锁之前,如果队列head后继(head.next)是独占节点时阻塞。

static final class NonfairSync extends Sync {

private static final long serialVersionUID = -8159625535654395037L;

final boolean writerShouldBlock() {

//写线程在抢锁之前永远不会阻塞-非公平锁

return false; // writers can always barge

}

final boolean readerShouldBlock() {

  • 读线程抢锁的时候,如果队列第一个是实质性节点(head.next)是独占节点时阻塞

  • 返回true是阻塞

*/

return apparentlyFirstQueuedIsExclusive();

}

}

/**

  • 判断qas队列的第一个元素是否是独占线程(写线程)

  • @return

*/

//AbstractQueuedSynchronizer

final boolean apparentlyFirstQueuedIsExclusive() {

Node h, s;

return (h = head) != null &&

(s = h.next) != null &&

!s.isShared() &&

s.thread != null;

}

final boolean isShared() {

return nextWaiter == SHARED;

}

2、FairSync

FairSync,无论是写线程还是读线程,只要同步队列中有其他节点在等待锁,就阻塞,这就是公平性。

static final class FairSync extends Sync {

private static final long serialVersionUID = -2274990926593161451L;

/**

  • 同步队列中head后继(head.next)不是当前线程时阻塞,

  • 即同步队列中有其他节点在等待锁,此时当前写线程阻塞

  • @return

*/

final boolean writerShouldBlock() {

return hasQueuedPredecessors();

}

/**

  • 同步队列中排在第一个实质性节点(head.next)不是当前线程时阻塞,

  • 即同步队列中有其他节点在等待锁,此时当前读线程阻塞

  • @return

*/

final boolean readerShouldBlock() {

return hasQueuedPredecessors();

}

}

//同步队列中head后继(head.next)是不是当前线程

public final boolean hasQueuedPredecessors() {

// The correctness of this depends on head being initialized

// before tail and on head.next being accurate if the current

// thread is first in queue.

Node t = tail; // Read fields in reverse initialization order

Node h = head;

Node s;

return h != t &&

((s = h.next) == null || s.thread != Thread.currentThread());

}

五、读锁的获取与释放


ReadLockWriteLock分别实现了Lock的lock和unlock等方法,实际上都是调用的ReentrantReadWriteLock.Sync的中已经实现好的模板方法。

1、ReadLock#lock

读锁是共享锁,首先尝试获取锁tryAcquireShared,获取锁失败则进入同步队列操作doAcquireShared

//ReentrantReadWriteLock.ReadLock#lock

public void lock() {

sync.acquireShared(1);

}

//AbstractQueuedSynchronizer#acquireShared

public final void acquireShared(int arg) {

//tryAcquireShared 返回-1 获取锁失败,1获取锁成功

if (tryAcquireShared(arg) < 0)

//获取锁失败入同步队列

doAcquireShared(arg);

}

(1)tryAcquireShared获取共享锁

tryAcquireSharedReentrantReadWriteLock#Sync中实现的。如下是获取共享锁的基本流程:

  1. 判断state,是否有线程持有写锁,若有且持有锁的不是当前线程,则返回-1,获取锁失败。(读写互斥)

  2. 若持有写锁的是当前线程,或者没有线程持有写锁,接下来判断读线程是否应该阻塞readerShouldBlock()

  3. readerShouldBlock()区分公平性,非公平锁,队列head后继(head.next)是独占节点,则阻塞;公平锁,队列中有其他节点在等待锁,则阻塞。

  4. 读线程不阻塞且加锁次数不超过MAX_COUNTCAS拿读锁成功c + SHARED_UNIT

  5. r = sharedCount(c)=0说明没有线程持有读锁,此时设置firstReader为当前线程,第一个读线程重入次数firstReaderHoldCount为1。

  6. r = sharedCount(c)!=0说明有线程持有读锁,此时当前线程是firstReader,则firstReaderHoldCount+1。

  7. 若持有当前读锁的不是firstReader,则HoldCounter来记录各个线程的读锁重入次数。

  8. 若因为CAS获取读锁失败,会进行自旋获取读锁fullTryAcquireShared(current)

//ReentrantReadWriteLock.Sync#tryAcquireShared

protected final int tryAcquireShared(int unused) {

Thread current = Thread.currentThread();

int c = getState();

//exclusiveCount© != 0 写锁被某线程持有,当前持有锁的线程不是当前线程,直接返回-1

if (exclusiveCount© != 0 &&

getExclusiveOwnerThread() != current)

return -1;

//写锁没有线程持有或者当前持有写锁的写线程可以继续拿读锁

int r = sharedCount©;

//nonFairSync 队列第一个是写线程时,读阻塞,!false && 加锁次数不超过max && CAS拿读锁,高16位+1

//FairSync 当队列的第一个不是当前线程时,阻塞。。。

if (!readerShouldBlock() &&

r < MAX_COUNT &&

compareAndSetState(c, c + SHARED_UNIT)) {

if (r == 0) { //r==0说明是第一个拿到读锁的读线程

firstReader = current;

firstReaderHoldCount = 1;

} else if (firstReader == current) { //第一个持有读锁的线程时当前线程,为重入

firstReaderHoldCount++;

} else {

//HoldCounter 记录线程重入锁的次数

//读锁 可以多个读线程持有,所以会记录持有读锁的所有读线程和分别重入次数

HoldCounter rh = cachedHoldCounter;

//rh==null 从readHolds获取

//rh != null rh.tid != 当前线程

if (rh == null || rh.tid != getThreadId(current))

//取出当前线程的cachedHoldCounter

cachedHoldCounter = rh = readHolds.get();

else if (rh.count == 0)

readHolds.set(rh);

rh.count++;

}

return 1;

}

//compareAndSetState(c, c + SHARED_UNIT) 失败后 自旋尝试获取锁

return fullTryAcquireShared(current);

}

(2)fullTryAcquireShared自旋获取共享锁

自旋获取锁的过程与tryAcquireShared类似,获取读锁,记录重入,只不过加了一个循环,循环结束的条件是获取锁成功(1)或者不满足获取锁的条件(-1)。

final int fullTryAcquireShared(Thread current) {

/*

  • This code is in part redundant with that in

  • tryAcquireShared but is simpler overall by not

  • complicating tryAcquireShared with interactions between

  • retries and lazily reading hold counts.

*/

HoldCounter rh = null;

for (;😉 {

int c = getState();

if (exclusiveCount© != 0) {

//写锁有线程持有,但是持锁的线程不是当前线程,返回-1,结束自旋。

if (getExclusiveOwnerThread() != current)

return -1;

// else we hold the exclusive lock; blocking here

// would cause deadlock.

} else if (readerShouldBlock()) {

//读线程应该被阻塞

// Make sure we’re not acquiring read lock reentrantly

if (firstReader == current) {

// assert firstReaderHoldCount > 0;

} else {

if (rh == null) {

rh = cachedHoldCounter;

if (rh == null || rh.tid != getThreadId(current)) {

rh = readHolds.get();

if (rh.count == 0)

//删除不持有该读锁的cachedHoldCounter

readHolds.remove();

}

}

if (rh.count == 0)

//当前线程不持有锁,直接返回-1

return -1;

}

}

//读线程不应该阻塞,判断state

if (sharedCount© == MAX_COUNT)

throw new Error(“Maximum lock count exceeded”);

//获取读锁

if (compareAndSetState(c, c + SHARED_UNIT)) {

//下面就是记录重入的机制了

if (sharedCount© == 0) {

firstReader = current;

firstReaderHoldCount = 1;

} else if (firstReader == current) {

firstReaderHoldCount++;

} else {

if (rh == null)

rh = cachedHoldCounter;

if (rh == null || rh.tid != getThreadId(current))

rh = readHolds.get();

else if (rh.count == 0)

readHolds.set(rh);

rh.count++;

cachedHoldCounter = rh; // cache for release

}

return 1;

}

}

}

(3)doAcquireShared进入同步队列操作

tryAcquireShared(arg)返回-1,获取锁失败,则进入同步队列操作:

  1. 创建一个共享节点,并拼接到同步队列尾部。

  2. 获取新节点的前继节点,若是head,则尝试获取锁。

  3. 获取锁成功,唤醒后继共享节点并出队列。

  4. node的前继节点不是head,或者获取锁失败,判断是否应该阻塞(shouldParkAfterFailedAcquire),应该阻塞parkAndCheckInterrupt阻塞当前线程。

private void doAcquireShared(int arg) {

//创建一个读节点,并入队列

final Node node = addWaiter(Node.SHARED);

boolean failed = true;

try {

boolean interrupted = false;

for (;😉 {

final Node p = node.predecessor();

if (p == head) {

//如果前继节点是head,则尝试获取锁

int r = tryAcquireShared(arg);

if (r >= 0) {

//获取锁成功,node出队列,

//唤醒其后继共享节点的线程

setHeadAndPropagate(node, r);

p.next = null; // help GC

if (interrupted)

selfInterrupt();

failed = false;

return;

}

}

/**

  • p不是头结点 or 获取锁失败,判断是否应该被阻塞

  • 前继节点的ws = SIGNAL 时应该被阻塞

*/

if (shouldParkAfterFailedAcquire(p, node) &&

parkAndCheckInterrupt())

interrupted = true;

}

} finally {

if (failed)

cancelAcquire(node);

}

}

这里的setHeadAndPropagate()是在获取共享锁成功的情况下调用的,所以propagate>0,(tryAcquireSharedSemaphore中有返回0的情况,返回结果为资源剩余量)。若node的下一个节点是共享节点,则调用doReleaseShared()唤醒后继节点。

(4)setHeadAndPropagate传播唤醒后继共享节点
  • 首先获取锁的node节点赋值给head,成为新head。

  • ReetrantReadWriteLock中node获取锁成功只有可能是propagate > 0,所以后面新旧head判断会省略,可以暂时不用考虑。

  • 若node后面没有节点(调用doReleaseShared没多大意义),或者node后面有节点且是共享节点则会调用doReleaseShared()唤醒后继节点。

(共享锁的传播性,详解请移步《AQS源码解读(六)——从PROPAGATE和setHeadAndPropagate()分析共享锁的传播性》。)

private void setHeadAndPropagate(Node node, int propagate) {

Node h = head; // Record old head for check below

setHead(node);

// propagate > 0 获取锁成功

// propagate < 0 获取锁失败,队列不为空,h.waitStatus < 0

if (propagate > 0 || h == null || h.waitStatus < 0 ||

(h = head) == null || h.waitStatus < 0) {

//唤醒后继共享节点

Node s = node.next;

if (s == null || s.isShared())

doReleaseShared();

}

}

2、ReadLock#lockInterruptibly

可中断获取锁,顾名思义就是获取锁的过程可响应中断。ReadLock#lockInterruptibly在获取锁的过程中有被中断(Thread.interrupted()),则会抛出异常InterruptedException,终止操作;其直接调用了AQS的模板方法acquireSharedInterruptibly

acquireSharedInterruptiblydoAcquireSharedInterruptibly详解请移步《AQS源码解读(五)——从acquireShared探索共享锁实现原理,何为共享?如何共享?》

//ReentrantReadWriteLock.ReadLock#lockInterruptibly

public void lockInterruptibly() throws InterruptedException {

sync.acquireSharedInterruptibly(1);

}

//AbstractQueuedSynchronizer#acquireSharedInterruptibly

public final void acquireSharedInterruptibly(int arg)

throws InterruptedException {

if (Thread.interrupted())

//被打断 抛出异常

throw new InterruptedException();

if (tryAcquireShared(arg) < 0)

//获取锁失败,进入队列操作

doAcquireSharedInterruptibly(arg);

}

3、ReadLock#tryLock

ReadLock#tryLoc尝试获取锁,调用的是ReentrantReadWriteLock.Sync实现的tryReadLock,获取锁成功返回true,失败返回false,不会进入队列操作,所以也不区分公平性。代码结构上和ReentrantReadWriteLock.Sync#tryAcquireShared相似,所以不多赘述,不同之处是ReadLock#tryLoc本身就是自旋获取锁。

public boolean tryLock() {

return sync.tryReadLock();

}

//ReentrantReadWriteLock.Sync#tryReadLock

final boolean tryReadLock() {

Thread current = Thread.currentThread();

for (;😉 {

int c = getState();

//判断是否有线程持有写锁

if (exclusiveCount© != 0 &&

getExclusiveOwnerThread() != current)

return false;

int r = sharedCount©;

if (r == MAX_COUNT)

throw new Error(“Maximum lock count exceeded”);

//没有线程持有写锁or持有写锁的是当前线程,写锁–>读锁 锁降级

if (compareAndSetState(c, c + SHARED_UNIT)) {

//获取读锁成功

if (r == 0) {

//第一个读锁的线程

firstReader = current;

firstReaderHoldCount = 1;

} else if (firstReader == current) {

//不是第一次获取读锁 但是firstReader是当前线程,重入

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 true;

}

}

}

同样和ReentrantLock一样,ReadLock#tryLock也有一个重载方法,可传入一个超时时长timeout和一个时间单位TimeUnit,超时时长会被转为纳秒级。

public boolean tryLock(long timeout, TimeUnit unit)

throws InterruptedException {

return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));

}

//AbstractQueuedSynchronizer#tryAcquireSharedNanos

public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)

throws InterruptedException {

if (Thread.interrupted())

throw new InterruptedException();

return tryAcquireShared(arg) >= 0 ||

doAcquireSharedNanos(arg, nanosTimeout);

}

tryLock(long timeout, TimeUnit unit)直接调用了AQS的模板方法tryAcquireSharedNanos,也具备了响应中断,超时获取锁的功能:

  1. 若一开始获取锁tryAcquireShared失败则进入AQS同步队列doAcquireSharedNanos

  2. 进入同步队列后自旋1000纳秒,还没有获取锁且判断应该阻塞,则会阻塞一定时长。

  3. 超时时长到线程自动唤醒,再自旋还没获取锁,且判断超时则返回false。

  4. 自旋判断前驱是head,则尝试获取锁,获取成功,则出队,传播唤醒后继。

tryAcquireSharedNanos详解请看拙作《AQS源码解读(五)——从acquireShared探索共享锁实现原理,何为共享?如何共享?》

4、ReadLock#unlock

读锁释放很简单,释放共享唤醒后继,无需区分公平性;其直接调用的是AQS的releaseSharedReentrantReadWriteLock只需要实现tryReleaseShared即可。

public void unlock() {

sync.releaseShared(1);

}

public final boolean releaseShared(int arg) {

if (tryReleaseShared(arg)) {

//读锁释放唤醒后继节点

doReleaseShared();

return true;

}

return false;

}

ReentrantReadWriteLock.Sync#tryReleaseShared

ReentrantReadWriteLock中实现的tryReleaseShared需要全部释放锁,才会返回true,才会调用doReleaseShared唤醒后继。

//ReentrantReadWriteLock.Sync#tryReleaseShared

protected final boolean tryReleaseShared(int unused) {

Thread current = Thread.currentThread();

if (firstReader == current) {

/**

  • 持有读锁的第一个线程是当前线程,且重入次数为1,释放锁将firstReader=null

  • 否则 firstReaderHoldCount-1

*/

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 (;😉 {

int c = getState();

int nextc = c - SHARED_UNIT;

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.

//读写都空闲了 才唤醒后面

return nextc == 0;

}

}

六、写锁的获取与释放


1、WriteLock#lock

写锁的lock和ReentrantLock的lock逻辑类似都是调用AbstractQueuedSynchronizer#acquire,区别在于tryAcquire的实现。

public void lock() {

sync.acquire(1);

}

public final void acquire(int arg) {

//若没有抢到锁,则进入等待队列

if (!tryAcquire(arg) &&

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

//自己中断自己

selfInterrupt();

}

ReentrantReadWriteLock.Sync#tryAcquire

ReentrantLocktryAcquire是在NonfairSyncFairSync中实现的,ReetrantReadWriteLock是在Sync中实现的。

ReetrantReadWriteLock中有读写锁,所以要考虑读写互斥的情况,即读锁被持有,将直接返回false,获取锁失败,如下是基本流程:

  1. c = getState() != 0,说明有线程持有读锁或者写锁。

  2. 继续判断w = exclusiveCount(c) = 0,则说明有线程持有读锁,直接返回false,获取锁失败。

  3. w = exclusiveCount(c) != 0,说明有线程持有写锁,判断持有锁的线程是否是当前线程,是就重入。

  4. c = getState() = 0,没有线程持有锁,判断writerShouldBlock,写线程是否应该阻塞,NonFairSync中 写线程无论如何都不应该阻塞,则继续抢锁;FairSync中的只要同步队列中有其他线程在排队,就应该阻塞。

  5. 最后若获取锁成功会设置持锁的线程为当前线程。

//ReentrantReadWriteLock.Sync#tryAcquire

protected final boolean tryAcquire(int acquires) {

Thread current = Thread.currentThread();

int c = getState();

int w = exclusiveCount©;

if (c != 0) { //c!=0 说明有读线程或者写线程持有锁

// (Note: if c != 0 and w == 0 then shared count != 0)

//w == 0 说明锁被读线程持有,w==0直接返回,抢锁失败,

//w != 0 判断当前线程是否持有锁,不是直接返回false,抢锁失败

if (w == 0 || current != getExclusiveOwnerThread())

return false;

//w!=0 && current == getExclusiveOwnerThread 当前线程重入

//首先判断重入次数是否超过最大次数

if (w + exclusiveCount(acquires) > MAX_COUNT)

throw new Error(“Maximum lock count exceeded”);

// Reentrant acquire

setState(c + acquires);

return true;

}

//没有线程持有锁,写线程是否应该被阻塞,

// FairSync中的是只要线程中有其他线程在排队,就阻塞

// NonFairSync 中 写线程抢锁无论如何都不阻塞,直接抢

if (writerShouldBlock() ||

!compareAndSetState(c, c + acquires))
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

《MySql面试专题》

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

《MySql性能优化的21个最佳实践》

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

《MySQL高级知识笔记》

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

文中展示的资料包括:**《MySql思维导图》《MySql核心笔记》《MySql调优笔记》《MySql面试专题》《MySql性能优化的21个最佳实践》《MySq高级知识笔记》**如下图

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

关注我,点赞本文给更多有需要的人
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
images/e5c14a7895254671a72faed303032d36.jpg" alt=“img” style=“zoom: 33%;” />

《MySql面试专题》

[外链图片转存中…(img-GCnB0e1e-1713064100513)]

[外链图片转存中…(img-IPFF1iKR-1713064100513)]

《MySql性能优化的21个最佳实践》

[外链图片转存中…(img-AWIQJY6s-1713064100514)]

[外链图片转存中…(img-53Ggr40e-1713064100514)]

[外链图片转存中…(img-njJKsMXw-1713064100514)]

[外链图片转存中…(img-IhVjTNFs-1713064100514)]

《MySQL高级知识笔记》

[外链图片转存中…(img-gII9B09E-1713064100514)]

[外链图片转存中…(img-34UglcMe-1713064100515)]

[外链图片转存中…(img-JUo9CKIZ-1713064100515)]

[外链图片转存中…(img-Rh09wU3B-1713064100515)]

[外链图片转存中…(img-QM3Dt5CY-1713064100516)]

[外链图片转存中…(img-QtfZyFeS-1713064100516)]

[外链图片转存中…(img-zKFQg5CG-1713064100516)]

[外链图片转存中…(img-wEya4IIh-1713064100516)]

[外链图片转存中…(img-pntMdTLQ-1713064100517)]

[外链图片转存中…(img-ID3w6aoT-1713064100517)]

文中展示的资料包括:**《MySql思维导图》《MySql核心笔记》《MySql调优笔记》《MySql面试专题》《MySql性能优化的21个最佳实践》《MySq高级知识笔记》**如下图

[外链图片转存中…(img-XyzRDG0Z-1713064100517)]

关注我,点赞本文给更多有需要的人
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值