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)

知其然不知其所以然,大厂常问面试技术如何复习?

1、热门面试题及答案大全

面试前做足功夫,让你面试成功率提升一截,这里一份热门350道一线互联网常问面试题及答案助你拿offer

2、多线程、高并发、缓存入门到实战项目pdf书籍

3、文中提到面试题答案整理

4、Java核心知识面试宝典

覆盖了JVM 、JAVA集合、JAVA多线程并发、JAVA基础、Spring原理、微服务、Netty与RPC、网络、日志、Zookeeper、Kafka、RabbitMQ、Hbase、MongoDB 、Cassandra、设计模式、负载均衡、数据库、一致性算法 、JAVA算法、数据结构、算法、分布式缓存、Hadoop、Spark、Storm的大量技术点且讲解的非常深入


《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
片转存中…(img-E7mTklY2-1713811414753)]

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

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

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

[外链图片转存中…(img-1SvOG7J0-1713811414753)]

知其然不知其所以然,大厂常问面试技术如何复习?

1、热门面试题及答案大全

面试前做足功夫,让你面试成功率提升一截,这里一份热门350道一线互联网常问面试题及答案助你拿offer

[外链图片转存中…(img-xveW52jY-1713811414754)]

2、多线程、高并发、缓存入门到实战项目pdf书籍

[外链图片转存中…(img-RNoJlJUT-1713811414754)]

[外链图片转存中…(img-GK2hFoR0-1713811414754)]

[外链图片转存中…(img-8Qn9NYBQ-1713811414754)]

3、文中提到面试题答案整理

[外链图片转存中…(img-DMLQXezd-1713811414755)]

4、Java核心知识面试宝典

覆盖了JVM 、JAVA集合、JAVA多线程并发、JAVA基础、Spring原理、微服务、Netty与RPC、网络、日志、Zookeeper、Kafka、RabbitMQ、Hbase、MongoDB 、Cassandra、设计模式、负载均衡、数据库、一致性算法 、JAVA算法、数据结构、算法、分布式缓存、Hadoop、Spark、Storm的大量技术点且讲解的非常深入

[外链图片转存中…(img-6lOrlpyl-1713811414755)]

[外链图片转存中…(img-GGFMbFbl-1713811414755)]

[外链图片转存中…(img-k0mBSfTx-1713811414756)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 16
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值