目录

一、用例

二、读写锁的核心思想

三、写锁的操作

3.1 写锁加锁-acquire

3.2 写锁-释放锁操作

四、读锁的操作

4.1 读锁的加锁操作

4.2 加锁-扔到队列准备阻塞操作


一、用例

将原来的锁,分割为两把锁:读锁、写锁。适用于读多写少的场景,读锁可以并发,写锁与其他锁互斥。写写互斥、写读互斥、读读兼容。

单个线程获取写锁后,再次获取读锁,可以拿到。(写读可重入)

单个线程获取读锁后,再次获取写锁,拿不到。(读写不可重入)

public class ThreadDemo {static volatile int a;public static void readA() {System.out.println("拿到读锁"+a);}public static void writeA() {System.out.println("拿到写锁");a++;}public static void main(String[] args) {ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();Thread readThread1 = new Thread(() -> {readLock.lock();try {readA();} finally {readLock.unlock();}});Thread readThread2 = new Thread(() -> {readLock.lock();try {readA();} finally {readLock.unlock();}});Thread writeThread = new Thread(() -> {writeLock.lock();try {writeA();} finally {writeLock.unlock();}});readThread1.start();readThread2.start();writeThread.start();}
}

二、读写锁的核心思想

ReentrantReadWriteLock还是基于AQS实现的。很多功能的实现和ReentrantLock类似

还是基于AQS的state来确定当前线程是否拿到锁资源

state表示读锁:将state的高16位作为读锁的标识

state表示写锁:将state的低16位作为写锁的标识

锁重入问题:

  • 写锁重入怎么玩:因为写操作和其他操作是互斥的,代表同一时间,只有一个线程持有着写锁,只要锁重入,就对低位+1即可。而且锁重入的限制,从原来的2^31 - 1,变为了2 ^ 16 -1。变短了~~

  • 读锁重入怎么玩:读锁的重入不能仿照写锁的方式,因为写锁属于互斥锁,同一时间只会有一个线程持有写锁,但是读锁是共享锁,同一时间会有多个线程持有读锁。所以每个获取到读锁的线程,记录锁重入的方式都是基于自己的ThreadLocal存储锁重入次数。

  • 读锁重入的时候就不操作state了?不对,每次锁重入还要修改state,只是记录当前线程锁重入的次数,需要基于ThreadLocal记录

  • 00000000 00000000 00000000 00000000 : state

    写锁:

    00000000 00000000 00000000 00000001

    写锁:

    00000000 00000000 00000000 00000010

    A读锁:拿不到,排队

    00000000 00000000 00000000 00000010

    写锁全部释放(唤醒)

    00000000 00000000 00000000 00000000

    A读锁:

    00000000 00000001 00000000 00000000

    B读锁:

    00000000 00000010 00000000 00000000

    B再次读锁:

    00000000 00000011 00000000 00000000

    每个读操作的线程,在获取读锁时,都需要开辟一个ThreadLocal。读写锁为了优化这个事情,做了两手操作:

  • 第一个拿到读锁的线程,不用ThreadLocal记录重入次数,在读写锁内有有一个firstRead记录重入次数

  • 还记录了最后一个拿到读锁的线程的重入次数,交给cachedHoldCounter属性标识,可以避免频繁的在锁重入时,从TL中获取

三、写锁的操作

3.1 写锁加锁-acquire

public final void acquire(int arg) {// 尝试获取锁资源(看一下,能否以CAS的方式将state 从0 ~ 1,改成功,拿锁成功)// 成功走人// 不成功执行下面方法if (!tryAcquire(arg) &&// addWaiter:将当前没按到锁资源的,封装成Node,排到AQS里// acquireQueued:当前排队的能否竞争锁资源,不能挂起线程阻塞acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}

因为都是AQS的实现,主要看tryAcquire

// state,高16:读,低16:写
00000000 00000000 00000000 0000000000000000 00000001 00000000 00000000 - SHARED_UNIT00000000 00000000 11111111 11111111 - MAX_COUNT00000000 00000000 11111111 11111111 - EXCLUSIVE_MASK
&
00000000 00000000 00000000 00000001 static final int SHARED_SHIFT   = 16;
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;// 只拿到表示读锁的高16位。
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
// 只拿到表示写锁的低16位。
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }// 读写锁的写锁,获取流程
protected final boolean tryAcquire(int acquires) {// 拿到当前线程Thread current = Thread.currentThread();// 拿到stateint c = getState();// 拿到了写锁的低16位标识wint w = exclusiveCount(c);// c != 0:要么有读操作拿着锁,要么有写操作拿着锁if (c != 0) {// 如果w == 0,代表没有写锁,拿不到!拜拜!// 如果w != 0,代表有写锁,看一下拿占用写锁是不是当前线程,如果不是,拿不到!拜拜!if (w == 0 || current != getExclusiveOwnerThread())return false;// 到这,说明肯定是写锁,并且是当前线程持有// 判断对低位 + 1,是否会超过MAX_COUNT,超过抛Errorif (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");// 如果没超过锁重入次数, + 1,返回true,拿到锁资源。setState(c + acquires);return true;}// 到这,说明c == 0// 读写锁也分为公平锁和非公平锁// 公平:看下排队不,排队就不抢了// 走hasQueuedPredecessors方法,有排队的返回true,没排队的返回false// 非公平:直接抢!// 方法实现直接返回falseif (writerShouldBlock() ||// 以CAS的方式,将state从0修改为 1!compareAndSetState(c, c + acquires))// 要么不让抢,要么CAS操作失败,返回falsereturn false;// 将当前持有互斥锁的线程,设置为自己setExclusiveOwnerThread(current);return true;
}

剩下的addWaiter和acquireQueued和ReentrantLock看的一样,都是AQS自身提供的方法

3.2 写锁-释放锁操作

读写锁的释放操作,跟ReentrantLock一致,只是需要单独获取低16位,判断是否为0,为0就释放成功

// 写锁的释放锁
public final boolean release(int arg) {// 只有tryRealse是读写锁重新实现的方法,其他的和ReentrantLock一致if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;
}// 读写锁的真正释放
protected final boolean tryRelease(int releases) {// 判断释放锁的线程是不是持有锁的线程if (!isHeldExclusively())// 不是抛异常throw new IllegalMonitorStateException();// 对state - 1int nextc = getState() - releases;// 拿着next从获取低16位的值,判断是否为0boolean free = exclusiveCount(nextc) == 0;// 返回trueif (free)// 将持有互斥锁的线程信息置位nullsetExclusiveOwnerThread(null);// 将-1之后的nextc复制给statesetState(nextc);return free;
}

四、读锁的操作

4.1 读锁的加锁操作

// 读锁加锁操作
public final void acquireShared(int arg) {// tryAcquireShared,尝试获取锁资源,获取到返回1,没获取到返回-1if (tryAcquireShared(arg) < 0)// doAcquireShared 前面没拿到锁,这边需要排队~doAcquireShared(arg);
}// tryAcquireShared方法
protected final int tryAcquireShared(int unused) {// 获取当前线程Thread current = Thread.currentThread();// 拿到stateint c = getState();// 那写锁标识,如果 !=0,代表有写锁if (exclusiveCount(c) != 0 &&// 如果持有写锁的不是当前线程,排队去!getExclusiveOwnerThread() != current)// 排队!return -1;// 没有写锁!// 获取读锁信息int r = sharedCount(c);// 公平锁: 有人排队,返回true,直接拜拜,没人排队,返回false// 非公平锁:正常的逻辑是非公平直接抢,因为是读锁,每次抢占只要CAS成功,必然成功// 这就会出现问题,写操作无法在读锁的情况抢占资源,导致写线程饥饿,一致阻塞…………// 非公平锁会查看next是否是写锁的,如果是,返回true,如果不是返回falseif (!readerShouldBlock() &&// 查看读锁是否已经达到了最大限制r < MAX_COUNT &&// 以CAS的方式,对state的高16位+1compareAndSetState(c, c + SHARED_UNIT)) {// 拿到锁资源成功!!!if (r == 0) {// 第一个拿到锁资源的线程,用first存储firstReader = current;firstReaderHoldCount = 1;} else if (firstReader == current) {// 我是锁重入,我就是第一个拿到读锁的线程,直接对firstReaderHoldCount++记录重入的次数firstReaderHoldCount++;} else {// 不是第一个拿到锁资源的// 先拿到cachedHoldCounter,最后一个线程的重入次数HoldCounter rh = cachedHoldCounter;// rh == null: 我是第二个拿到读锁的!// 或者发现之前有最后一个来的,但是不我,将我设置为最后一个。if (rh == null || rh.tid != getThreadId(current))// 获取自己的重入次数,并赋值给cachedHoldCountercachedHoldCounter = rh = readHolds.get();// 之前拿过,现在如果为0,赋值给TLelse if (rh.count == 0)readHolds.set(rh);// 重入次数+1,// 第一个:可能是第一次拿// 第二个:可能是重入操作rh.count++;}return 1;}return fullTryAcquireShared(current);
}// 通过tryAcquireShared没拿到锁资源,也没返回-1,就走这
final int fullTryAcquireShared(Thread current) {HoldCounter rh = null;for (;;) {// 拿stateint c = getState();// 现在有互斥锁,不是自己,拜拜!if (exclusiveCount(c) != 0) {if (getExclusiveOwnerThread() != current)return -1;// 公平:有排队的,进入逻辑。   没排队的,过!// 非公平:head的next是写不,是,进入逻辑。   如果不是,过!} else if (readerShouldBlock()) {// 这里代码特别乱,因为这里的代码为了处理JDK1.5的内存泄漏问题,修改过~// 这个逻辑里不会让你拿到锁,做被阻塞前的准备if (firstReader == current) {// 什么都不做} else {if (rh == null) {// 获取最后一个拿到读锁资源的rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current)) {// 拿到我自己的记录重入次数的。rh = readHolds.get();// 如果我的次数是0,绝对不是重入操作!if (rh.count == 0)// 将我的TL中的值移除掉,不移除会造成内存泄漏readHolds.remove();}}// 如果我的次数是0,绝对不是重入操作!if (rh.count == 0)// 返回-1,等待阻塞吧!return -1;}}// 超过读锁的最大值了没?if (sharedCount(c) == MAX_COUNT)throw new Error("Maximum lock count exceeded");// 到这,就CAS竞争锁资源if (compareAndSetState(c, c + SHARED_UNIT)) {// 跟tryAcquireShared一模一样if (sharedCount(c) == 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; }return 1;}}
}

4.2 加锁-扔到队列准备阻塞操作

// 没拿到锁,准备挂起
private void doAcquireShared(int arg) {// 将当前线程封装为Node,当前Node为共享锁,并添加到队列的模式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) {// 如果r大于等于0,代表获取锁资源成功// 唤醒AQS中我后面的要获取读锁的线程(SHARED模式的Node)setHeadAndPropagate(node, r);p.next = null; if (interrupted)selfInterrupt();failed = false;return;}}// 能否挂起当前线程,需要保证我前面Node的状态为-1,才能执行后面操作if (shouldParkAfterFailedAcquire(p, node) &&//LockSupport.park挂起~~parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}

学习笔记总结之JAVA JUC ReentrantReadWriteLock(四)相关推荐

  1. spring学习笔记(一)创建对象的四种方式

    spring学习笔记(一)创建对象的四种方式 一.简介 ​ Spring是一个轻量级控制反转(IoC)和面向切面(AOP)的容器框架. ​ 所谓IoC就是Iversion of Control,控制反 ...

  2. R语言学习笔记——高级篇:第十四章-主成分分析和因子分析

    R语言 R语言学习笔记--高级篇:第十四章-主成分分析和因子分析 文章目录 R语言 前言 一.R中的主成分和因子分析 二.主成分分析 2.1.判断主成分的个数 2.2.提取主成分 2.3.主成分旋转 ...

  3. Python学习笔记7:实操案例四(支付密码的验证,模拟QQ账号登录,商品价格竞猜,星座看运势)

    Python学习笔记7:实操案例四(支付密码的验证,模拟QQ账号登录,商品价格竞猜,星座看运势) 1.支付密码的验证: 这个主要就是调用isdigit()函数判断字符串是不是全是数字组成. pwd=i ...

  4. JavaScript学习笔记(第二部分)总共四部分

    JavaScript学习笔记(第二部分)总共四部分 4 对象(Object) 字符串String.数值Number.布尔值Boolean.空值Null.未定义Undefined是基本的数据类型,这些数 ...

  5. 20W字纯手打Java并发学习笔记,助力你金三银四,决战春招,必进大厂

    假如阿里给了你这个机会,你却卡在三面,你会不会懊恼? 假如阿里真的让你通过,只需要你把这一块技能的底层原理摸透,你学不学? 我有一个朋友,他小厂背景.15年毕业.普通学校,这看起来确实没什么战斗力,但 ...

  6. [原创]java WEB学习笔记36:Java Bean 概述,及在JSP 中的使用,原理

    本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当 ...

  7. 【HBase学习笔记-尚硅谷-Java API shell命令 谷粒微博案例】

    HBase学习笔记 HBase 一.HBase简介 1.HBase介绍 2.HBase的逻辑结构和物理结构 3.数据模型 4.基本架构 二.快速入门 1.配置HBase 2.命令 三.API 1.获取 ...

  8. 1.1.10 从二分查找BinarySearch开启算法学习之路---《java算法第四版》

    文章目录 0.前言 1.功能 2.示例 有两个名单tinyW.txt和tinyT.txt,将tinyT.txt名单中不在tinyW.txt的数据打印出来 ① 实现原理 ② 实现代码 ③ 性能分析 0. ...

  9. 【OS学习笔记】十六 保护模式四:进入保护模式与在保护模式下访问内存的汇编代码

    本文记录的是之前四篇文章所对应的汇编代码.四篇文章分别是: [OS学习笔记]十二 现代处理器的结构和特点 [OS学习笔记]十三 保护模式一:全局描述符表(GDT) [OS学习笔记]十四 保护模式二:段 ...

最新文章

  1. 中科院aibench_中科院发布目标追踪数据集,万条视频,150万个边界框 | 快来下载...
  2. RPA女子计划—面向日本女性的工作方式改革
  3. 抽象类和接口-手机小案例
  4. k8s Dashboard部署Tomcat集群
  5. secureCRT使用退格键(backspace)出现^H解决的方法
  6. oracle11g临时表,oracle11G的临时表空间
  7. 一个demo学会c++编程
  8. 迈信EP100伺服驱动器量产型修改bug全套C源代码
  9. 什么是bug?bug的分类
  10. 零预算也能用SEO技巧达到Google自然搜寻结果第1名
  11. 二舅治好我的精神内耗,也让我火出了B站
  12. Tomcat启动之后遇到“ran out of the normal time range, it consumed [2000] milliseconds.”?
  13. 关于龙勃透镜天线,看这一篇就够了!
  14. Java使用Calender类实现打印日历(指定月份和年)
  15. 分辨mqtt在线与离线_最全视频下载方案,100%下载所有在线视频!
  16. 均线系统:6种走势模型、买卖信号、仓位控制
  17. 半路出家如何玩转编程
  18. Google gflags
  19. QuotaExceededError the quota has been exceeded --- Firefox 报错解决
  20. Latex写作心得-投稿至Elsevier

热门文章

  1. 立创商城购物车的导入与导出
  2. Linux C/C++开发、嵌入式软件开发面试记录 ( 五)
  3. 散户做外汇买卖怎样稳定的获利
  4. 开源能翻译英文的Android阅读器
  5. android列表字母排序,Android 实现ListView的A-Z字母排序和过滤搜索功能,实现汉字转成拼音...
  6. 条码软件如何在标签纸上插入人民币符号
  7. QPolygon/QPolygonF方法功能(QT5.12)
  8. QTYX量化系统实战案例分享|箱底形态选股后潜伏介入之202209
  9. 关于ArcGIS的使用以及 项目:将点云数据转为TIN显示
  10. plc中MB和VB的区别