Java 读写锁 ReentrantReadWriteLock 源码分析,headfirstjavapdf百度云

HarmonyOS学习 2021-11-25 19:07:19
java 面试 编程语言 后端开发

  1. cachedHoldCounter?有什么用?其实没什么用,但能提示性能。将最后一次获取读锁的线程的 HoldCounter 缓存到这里,这样比使用 ThreadLocal 性能要好一些,因为 ThreadLocal 内部是基于 map 来查询的。但是 cachedHoldCounter 这一个属性毕竟只能缓存一个线程,所以它要起提升性能作用的依据就是:通常读锁的获取紧随着就是该读锁的释放。我这里可能表达不太好,但是大家应该是懂的吧。

  2. firstReader?和?firstReaderHoldCount?有什么用?其实也没什么用,但是它也能提示性能。将"第一个"获取读锁的线程记录在 firstReader 属性中,这里的第一个不是全局的概念,等这个 firstReader 当前代表的线程释放掉读锁以后,会有后来的线程占用这个属性的。firstReader 和 firstReaderHoldCount 使得在读锁不产生竞争的情况下,记录读锁重入次数非常方便快速

  3. 如果一个线程使用了 firstReader,那么它就不需要占用 cachedHoldCounter

  4. 个人认为,读写锁源码中最让初学者头疼的就是这几个用于提升性能的属性了,使得大家看得云里雾里的。主要是因为 ThreadLocal 内部是通过一个 ThreadLocalMap 来操作的,会增加检索时间。而很多场景下,执行 unlock 的线程往往就是刚刚最后一次执行 lock 的线程,中间可能没有其他线程进行 lock。还有就是很多不怎么会发生读锁竞争的场景。

上面说了这么多,是希望能帮大家降低后面阅读源码的压力,大家也可以先看看后面的,然后再慢慢体会。

前面我们好像都只说读锁,完全没提到写锁,主要是因为写锁真的是简单很多,我也特地将写锁的源码放到了后面,我们先啃下最难的读锁先。

读锁获取

下面我就不一行一行按源码顺序说了,我们按照使用来说。

我们来看下读锁 ReadLock 的 lock 流程:

// ReadLock

public void lock() {

sync.acquireShared(1);

}

// AQS

public final void acquireShared(int arg) {

if (tryAcquireShared(arg) < 0)

doAcquireShared(arg);

}

然后我们就会进到 Sync 类的 tryAcquireShared 方法:

在 AQS 中,如果 tryAcquireShared(arg) 方法返回值小于 0 代表没有获取到共享锁(读锁),大于 0 代表获取到

回顾 AQS 共享模式:tryAcquireShared 方法不仅仅在 acquireShared 的最开始被使用,这里是 try,也就可能会失败,如果失败的话,执行后面的 doAcquireShared,进入到阻塞队列,然后等待前驱节点唤醒。唤醒以后,还是会调用 tryAcquireShared 进行获取共享锁的。当然,唤醒以后再 try 是很容易获得锁的,因为这个节点已经排了很久的队了,组织是会照顾它的。

所以,你在看下面这段代码的时候,要想象到两种获取读锁的场景,一种是新来的,一种是排队排到它的。

protected final int tryAcquireShared(int unused) {

Thread current = Thread.currentThread();

int c = getState();

// exclusiveCount 不等于 0,说明有线程持有写锁,

// 而且不是当前线程持有写锁,那么当前线程获取读锁失败

// (另,如果持有写锁的是当前线程,是可以继续获取读锁的)

if (exclusiveCount != 0 &&

getExclusiveOwnerThread() != current)

return -1;

// 读锁的获取次数

int r = sharedCount;

// 读锁获取是否需要被阻塞,稍后细说。为了进去下面的分支,假设这里不阻塞就好了

if (!readerShouldBlock() &&

//

《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》

【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享

判断是否会溢出 (2^16-1,没那么容易溢出的)

r < MAX_COUNT &&

// 下面这行 CAS 是将 state 属性的高 16 位加 1,低 16 位不变,如果成功就代表获取到了读锁

compareAndSetState(c, c + SHARED_UNIT)) {

// =======================

// 进到这里就是获取到了读锁

// =======================

if (r == 0) {

// r == 0 说明此线程是第一个获取读锁的,或者说在它前面获取读锁的都走光光了,它也算是第一个吧

// 记录 firstReader 为当前线程,及其持有的读锁数量:1

firstReader = current;

firstReaderHoldCount = 1;

} else if (firstReader == current) {

// 进来这里,说明是 firstReader 重入获取读锁(这非常简单,count 加 1 结束)

firstReaderHoldCount++;

} else {

// 前面我们说了 cachedHoldCounter 用于缓存最后一个获取读锁的线程

// 如果 cachedHoldCounter 缓存的不是当前线程,设置为缓存当前线程的 HoldCounter

HoldCounter rh = cachedHoldCounter;

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

cachedHoldCounter = rh = readHolds.get();

else if (rh.count == 0)

// 到这里,那么就是 cachedHoldCounter 缓存的是当前线程,但是 count 为 0,

// 大家可以思考一下:这里为什么要 set ThreadLocal 呢?(当然,答案肯定不在这块代码中)

// 既然 cachedHoldCounter 缓存的是当前线程,

// 当前线程肯定调用过 readHolds.get() 进行初始化 ThreadLocal

readHolds.set(rh);

// count 加 1

rh.count++;

}

// return 大于 0 的数,代表获取到了共享锁

return 1;

}

// 往下看

return fullTryAcquireShared(current);

}

上面的代码中,要进入 if 分支,需要满足:readerShouldBlock() 返回 false,并且 CAS 要成功(我们先不要纠结 MAX_COUNT 溢出)。

那我们反向推,怎么样进入到最后的 fullTryAcquireShared:

  • readerShouldBlock() 返回 true,2 种情况:

  • 在 FairSync 中说的是 hasQueuedPredecessors(),即阻塞队列中有其他元素在等待锁。

也就是说,公平模式下,有人在排队呢,你新来的不能直接获取锁

  • 在 NonFairSync 中说的是 apparentlyFirstQueuedIsExclusive(),即判断阻塞队列中 head 的第一个后继节点是否是来获取写锁的,如果是的话,让这个写锁先来,避免写锁饥饿。

作者给写锁定义了更高的优先级,所以如果碰上获取写锁的线程马上就要获取到锁了,获取读锁的线程不应该和它抢。

如果 head.next 不是来获取写锁的,那么可以随便抢,因为是非公平模式,大家比比 CAS 速度

  • compareAndSetState(c, c + SHARED_UNIT) 这里 CAS 失败,存在竞争。可能是和另一个读锁获取竞争,当然也可能是和另一个写锁获取操作竞争。

然后就会来到 fullTryAcquireShared 中再次尝试:

/**

* 1. 刚刚我们说了可能是因为 CAS 失败,如果就此返回,那么就要进入到阻塞队列了,

* 想想有点不甘心,因为都已经满足了 !readerShouldBlock(),也就是说本来可以不用到阻塞队列的,

* 所以进到这个方法其实是增加 CAS 成功的机会

* 2. 在 NonFairSync 情况下,虽然 head.next 是获取写锁的,我知道它等待很久了,我没想和它抢,

* 可是如果我是来重入读锁的,那么只能表示对不起了

*/

final int fullTryAcquireShared(Thread current) {

HoldCounter rh = null;

// 别忘了这外层有个 for 循环

for (; {

int c = getState();

// 如果其他线程持有了写锁,自然这次是获取不到读锁了,乖乖到阻塞队列排队吧

if (exclusiveCount != 0) {

if (getExclusiveOwnerThread() != current)

return -1;

// else we hold the exclusive lock; blocking here

// would cause deadlock.

} else if (readerShouldBlock()) {

/**

* 进来这里,说明:

* 1. exclusiveCount == 0:写锁没有被占用

* 2. readerShouldBlock() 为 true,说明阻塞队列中有其他线程在等待

*

* 既然 should block,那进来这里是干什么的呢?

* 答案:是进来处理读锁重入的!

*

*/

// firstReader 线程重入读锁,直接到下面的 CAS

if (firstReader == current) {

// assert firstReaderHoldCount > 0;

} else {

if (rh == null) {

rh = cachedHoldCounter;

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

// cachedHoldCounter 缓存的不是当前线程

// 那么到 ThreadLocal 中获取当前线程的 HoldCounter

// 如果当前线程从来没有初始化过 ThreadLocal 中的值,get() 会执行初始化

rh = readHolds.get();

// 如果发现 count == 0,也就是说,纯属上一行代码初始化的,那么执行 remove

// 然后往下两三行,乖乖排队去

if (rh.count == 0)

readHolds.remove();

}

}

if (rh.count == 0)

// 排队去。

return -1;

}

/**

* 这块代码我看了蛮久才把握好它是干嘛的,原来只需要知道,它是处理重入的就可以了。

* 就是为了确保读锁重入操作能成功,而不是被塞到阻塞队列中等待

*

* 另一个信息就是,这里对于 ThreadLocal 变量 readHolds 的处理:

* 如果 get() 后发现 count == 0,居然会做 remove() 操作,

* 这行代码对于理解其他代码是有帮助的

*/

}

if (sharedCount == MAX_COUNT)

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

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

// 这里 CAS 成功,那么就意味着成功获取读锁了

// 下面需要做的是设置 firstReader 或 cachedHoldCounter

if (sharedCount == 0) {

// 如果发现 sharedCount 等于 0,就将当前线程设置为 firstReader

firstReader = current;

firstReaderHoldCount = 1;

} else if (firstReader == current) {

firstReaderHoldCount++;

} else {

// 下面这几行,就是将 cachedHoldCounter 设置为当前线程

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;

}

// 返回大于 0 的数,代表获取到了读锁

return 1;

}

}

}

firstReader 是每次将读锁获取次数从 0 变为 1 的那个线程。

能缓存到 firstReader 中就不要缓存到 cachedHoldCounter 中。

上面的源码分析应该说得非常详细了,如果到这里你不太能看懂上面的有些地方的注释,那么可以先往后看,然后再多看几遍。

读锁释放

下面我们看看读锁释放的流程:

// ReadLock

public void unlock() {

sync.releaseShared(1);

}

// Sync

public final boolean releaseShared(int arg) {

if (tryReleaseShared(arg)) {

doReleaseShared(); // 这句代码其实唤醒 获取写锁的线程,往下看就知道了

return true;

}

return false;

}

// Sync

protected final boolean tryReleaseShared(int unused) {

Thread current = Thread.currentThread();

if (firstReader == current) {

if (firstReaderHoldCount == 1)

// 如果等于 1,那么这次解锁后就不再持有锁了,把 firstReader 置为 null,给后来的线程用

// 为什么不顺便设置 firstReaderHoldCount = 0?因为没必要,其他线程使用的时候自己会设值

firstReader = null;

else

firstReaderHoldCount–;

} else {

// 判断 cachedHoldCounter 是否缓存的是当前线程,不是的话要到 ThreadLocal 中取

HoldCounter rh = cachedHoldCounter;

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

rh = readHolds.get();

int count = rh.count;

if (count <= 1) {

// 这一步将 ThreadLocal remove 掉,防止内存泄漏。因为已经不再持有读锁了

readHolds.remove();

if (count <= 0)

// 就是那种,lock() 一次,unlock() 好几次的逗比

throw unmatchedUnlockException();

}

// count 减 1

–rh.count;

}

for (; {

int c = getState();

// nextc 是 state 高 16 位减 1 后的值

int nextc = c - SHARED_UNIT;

if (compareAndSetState(c, nextc))

// 如果 nextc == 0,那就是 state 全部 32 位都为 0,也就是读锁和写锁都空了

// 此时这里返回 true 的话,其实是帮助唤醒后继节点中的获取写锁的线程

return nextc == 0;

}

}

读锁释放的过程还是比较简单的,主要就是将 hold count 减 1,如果减到 0 的话,还要将 ThreadLocal 中的 remove 掉。

然后是在 for 循环中将 state 的高 16 位减 1,如果发现读锁和写锁都释放光了,那么唤醒后继的获取写锁的线程。

写锁获取

  1. 写锁是独占锁。

  2. 如果有读锁被占用,写锁获取是要进入到阻塞队列中等待的。

// WriteLock

public void lock() {

sync.acquire(1);

}

// AQS

public final void acquire(int arg) {

if (!tryAcquire(arg) &&

// 如果 tryAcquire 失败,那么进入到阻塞队列等待

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

selfInterrupt();

}

// Sync

protected final boolean tryAcquire(int acquires) {

Thread current = Thread.currentThread();

int c = getState();

int w = exclusiveCount;

if (c != 0) {

// 看下这里返回 false 的情况:

// c != 0 && w == 0: 写锁可用,但是有线程持有读锁(也可能是自己持有)

// c != 0 && w !=0 && current != getExclusiveOwnerThread(): 其他线程持有写锁

// 也就是说,只要有读锁或写锁被占用,这次就不能获取到写锁

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

return false;

if (w + exclusiveCount(acquires) > MAX_COUNT)

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

// 这里不需要 CAS,仔细看就知道了,能到这里的,只可能是写锁重入,不然在上面的 if 就拦截了

setState(c + acquires);

return true;

}

// 如果写锁获取不需要 block,那么进行 CAS,成功就代表获取到了写锁

if (writerShouldBlock() ||

!compareAndSetState(c, c + acquires))

return false;

setExclusiveOwnerThread(current);

return true;

}

下面看一眼?writerShouldBlock()?的判定,然后你再回去看一篇写锁获取过程。

static final class NonfairSync extends Sync {

// 如果是非公平模式,那么 lock 的时候就可以直接用 CAS 去抢锁,抢不到再排队

final boolean writerShouldBlock() {

return false; // writers can always barge

}

}

static final class FairSync extends Sync {

final boolean writerShouldBlock() {

// 如果是公平模式,那么如果阻塞队列有线程等待的话,就乖乖去排队

return hasQueuedPredecessors();

}

}

写锁释放

// WriteLock

public void unlock() {

sync.release(1);

}

// AQS

public final boolean release(int arg) {

// 1. 释放锁

if (tryRelease(arg)) {

// 2. 如果独占锁释放"完全",唤醒后继节点

Node h = head;

if (h != null && h.waitStatus != 0)

unparkSuccessor(h);

return true;

}

return false;

}

// Sync

// 释放锁,是线程安全的,因为写锁是独占锁,具有排他性

// 实现很简单,state 减 1 就是了

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

// 如果 exclusiveCount(nextc) == 0,也就是说包括重入的,所有的写锁都释放了,

// 那么返回 true,这样会进行唤醒后继节点的操作。

return free;

}

看到这里,是不是发现写锁相对于读锁来说要简单很多。

锁降级


Doug Lea 没有说写锁更高级,如果有线程持有读锁,那么写锁获取也需要等待。

不过从源码中也可以看出,确实会给写锁一些特殊照顾,如非公平模式下,为了提高吞吐量,lock 的时候会先 CAS 竞争一下,能成功就代表读锁获取成功了,但是如果发现 head.next 是获取写锁的线程,就不会去做 CAS 操作。

Doug Lea 将持有写锁的线程,去获取读锁的过程称为锁降级(Lock downgrading)。这样,此线程就既持有写锁又持有读锁。

但是,锁升级是不可以的。线程持有读锁的话,在没释放的情况下不能去获取写锁,因为会发生死锁

总结

总体来说,如果你想转行从事程序员的工作,Java开发一定可以作为你的第一选择。但是不管你选择什么编程语言,提升自己的硬件实力才是拿高薪的唯一手段。

如果你以这份学习路线来学习,你会有一个比较系统化的知识网络,也不至于把知识学习得很零散。我个人是完全不建议刚开始就看《Java编程思想》、《Java核心技术》这些书籍,看完你肯定会放弃学习。建议可以看一些视频来学习,当自己能上手再买这些书看又是非常有收获的事了。

Java 读写锁 ReentrantReadWriteLock 源码分析,headfirstjavapdf百度云_面试

本文已被 CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

版权声明
本文为[HarmonyOS学习]所创,转载请带上原文链接,感谢
https://blog.51cto.com/u_15438507/4690504

  1. jQuery-实例方法
  2. jQuery-dom和jQuery,入口函数(基本知识)
  3. JavaScript-this指向问题
  4. JavaScript-封装与继承(两种)
  5. JavaScript-包装类型
  6. JavaScript-传值(引用类型,基本类型)
  7. JavaScript-面向对象(构造函数,实例成员,静态成员)
  8. JavaScript-解构赋值
  9. JavaScript-箭头函数
  10. JavaScript-参数
  11. JavaScript-预解析(变量提升)
  12. JavaScript-闭包closure
  13. JavaScript-声明变量的关键字
  14. JavaScript - mot - clé pour déclarer une variable
  15. Fermeture de fermeture JavaScript
  16. JavaScript Pre - parse (promotion des variables)
  17. Paramètres JavaScript
  18. Fonction de flèche JavaScript
  19. JavaScript - déconstruction assignations
  20. Common annotations in springboot
  21. Building CentOS 7.6 with Linux
  22. JavaScript - orienté objet (constructeur, membre d'instance, membre statique)
  23. JavaScript value Transfer (reference type, Basic type)
  24. JavaScript - type d'emballage
  25. linux deepin/ubuntu安装flameshot火焰截图
  26. JavaScript - encapsulation et héritage (deux)
  27. JavaScript JS method for writing 99 multiplication table
  28. 從零開始學java - 第二十五天
  29. Apprendre Java à partir de zéro - jour 25
  30. Les voitures d'hiver, les voitures électriques et les voitures à essence ne sont pas les mêmes?
  31. JavaScript - ceci pointe vers le problème
  32. Copie JavaScript
  33. Spring boot quickly integrates swagger
  34. linux deepin/ubuntu安裝flameshot火焰截圖
  35. Capture d'écran de flamme de l'installateur de flamme Linux deepin / Ubuntu
  36. Jquery DOM et jquery, fonctions d'entrée (bases)
  37. Méthode d'instance jquery
  38. Méthode et démonstration de code dans l'interface de liste en Java
  39. 【错误记录】Java 中 ArrayList 排序 ( 使用 Comparator 接口时注意 compare 返回值是 -1 和 +1 )
  40. Démarrage du Zookeeper
  41. Java oom Cognition
  42. Java 开发者最困惑的四件事,值得一看!,BAT面试文档
  43. Java 将两个对象list里面的 某个字段值抽取到一个list里,java选择排序原理
  44. Java 多线程 —— 生产者消费者问题,从基础到深入案例
  45. Java 中设计模式 之 工厂模式,java反射机制的底层原理
  46. 【錯誤記錄】Java 中 ArrayList 排序 ( 使用 Comparator 接口時注意 compare 返回值是 -1 和 +1 )
  47. 【 enregistrement des erreurs 】 tri ArrayList en Java (Notez que les valeurs de retour de comparaison sont - 1 et + 1 lors de l'utilisation de l'interface de comparaison)
  48. Pourquoi Xiaopeng P5 est - il devenu un grand succès dans le cercle? Le Salon de l'automobile de Guangzhou montre encore des compétences polyvalentes
  49. Java 開發者最困惑的四件事,值得一看!,BAT面試文檔
  50. Les quatre choses les plus déroutantes pour les développeurs Java valent la peine d'être regardées! Document d'entrevue sur les MTD
  51. k8s-Pod污点与容忍
  52. k8s-Pod污点与容忍
  53. K8s POD Contamination and Tolerance
  54. K8s POD Contamination and Tolerance
  55. mysql常用语句——GROUP BY和HAVING
  56. Spring boot quickly integrates swagger
  57. mysql常用語句——GROUP BY和HAVING
  58. MySQL Common statements - group by and having
  59. Le défi du pinceau leetcode - Javascript: 110. Arbre binaire équilibré
  60. [notes d'apprentissage de première ligne] day44: XMLHttpRequest