JAVA并发十一:深入理解读写锁ReentrantReadWriteLock

读写锁ReentrantReadWriteLock详解 读写锁、ReentrantReadWriteLock 阅读详情


作者:Java菜鸟奋斗史,本文为原创投稿文章

欢迎点击阅读原文,直达原作者文章围观。

知音专栏

 

 程序员的出路

 关于资源视频下载的说明

【强化编程功底】算法文摘


1.读写锁的介绍

在并发场景中用于解决线程安全的问题,我们几乎会高频率的使用到独占式锁,通常使用java提供的关键字synchronized(关于synchronized可以看这篇文章)或者concurrents包中实现了Lock接口的ReentrantLock。它们都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁。


而在一些业务场景中,大部分只是读数据,写数据很少,如果仅仅是读数据的话并不会影响数据正确性(出现脏读),而如果在这种业务场景下,依然使用独占锁的话,很显然这将是出现性能瓶颈的地方。


针对这种读多写少的情况,java还提供了另外一个实现Lock接口的ReentrantReadWriteLock(读写锁)。读写所允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。


在分析WirteLock和ReadLock的互斥性时可以按照WriteLock与WriteLock之间,WriteLock与ReadLock之间以及ReadLock与ReadLock之间进行分析。更多关于读写锁特性介绍大家可以看源码上的介绍(阅读源码时最好的一种学习方式,我也正在学习中,与大家共勉),这里做一个归纳总结:

  • 公平性选择:支持非公平性(默认)和公平的锁获取方式,吞吐量还是非公平优于公平;

  • 重入性:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁;

  • 锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁


要想能够彻底的理解读写锁必须能够理解这样几个问题:

  1. 读写锁是怎样实现分别记录读写状态的?

  2. 写锁是怎样获取和释放的?

  3. 读锁是怎样获取和释放的?

我们带着这样的三个问题,再去了解下读写锁。


2.写锁详解

2.1.写锁的获取

同步组件的实现聚合了同步器(AQS),并通过重写重写同步器(AQS)中的方法实现同步组件的同步语义(关于同步组件的实现层级结构可以看这篇文章,AQS的底层实现分析可以看这篇文章)。


因此,写锁的实现依然也是采用这种方式。在同一时刻写锁是不能被多个线程所获取,很显然写锁是独占式锁,而实现写锁的同步语义是通过重写AQS中的tryAcquire方法实现的。源码为:

protected final boolean tryAcquire(int acquires) {
   /*
    * Walkthrough:
    * 1. If read count nonzero or write count nonzero
    *    and owner is a different thread, fail.
    * 2. If count would saturate, fail. (This can only
    *    happen if count is already nonzero.)
    * 3. Otherwise, this thread is eligible for lock if
    *    it is either a reentrant acquire or
    *    queue policy allows it. If so, update state
    *    and set owner.
    */

   Thread current = Thread.currentThread();
 // 1. 获取写锁当前的同步状态
   int c = getState();
 // 2. 获取写锁获取的次数
   int w = exclusiveCount(c);
   if (c != 0) {
       // (Note: if c != 0 and w == 0 then shared count != 0)
   // 3.1 当读锁已被读线程获取或者当前线程不是已经获取写锁的线程的话
   // 当前线程获取写锁失败
       if (w == 0 || current != getExclusiveOwnerThread())
           return false;
       if (w + exclusiveCount(acquires) > MAX_COUNT)
           throw new Error("Maximum lock count exceeded");
       // Reentrant acquire
   // 3.2 当前线程获取写锁,支持可重复加锁
       setState(c + acquires);
       return true;
   }
 // 3.3 写锁未被任何线程获取,当前线程可获取写锁
   if (writerShouldBlock() ||
       !compareAndSetState(c, c + acquires))
       return false;
   setExclusiveOwnerThread(current);
   return true;
}


这段代码的逻辑请看注释,这里有一个地方需要重点关注,exclusiveCount(c)方法,该方法源码为:


static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }


其中EXCLUSIVE_MASK为:  

static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;      EXCLUSIVE _MASK为1左移16位然后减1,即为0x0000FFFF。而exclusiveCount方法是将同步状态(state为int类型)与0x0000FFFF相与,即取同步状态的低16位。


那么低16位代表什么呢?根据exclusiveCount方法的注释为独占式获取的次数即写锁被获取的次数,现在就可以得出来一个结论:同步状态的低16位用来表示写锁的获取次数。同时还有一个方法值得我们注意:


static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }


该方法是获取读锁被获取的次数,是将同步状态(int c)右移16次,即取同步状态的高16位,现在我们可以得出另外一个结论同步状态的高16位用来表示读锁被获取的次数。


现在还记得我们开篇说的需要弄懂的第一个问题吗?读写锁是怎样实现分别记录读锁和写锁的状态的,现在这个问题的答案就已经被我们弄清楚了,其示意图如下图所示:

Java并发专题【11】深入理解读写锁ReentrantReadWriteLock


现在我们回过头来看写锁获取方法tryAcquire,其主要逻辑为:当读锁已经被读线程获取或者写锁已经被其他写线程获取,则写锁获取失败;否则,获取成功并支持重入,增加写状态。


2.2.写锁的释放

写锁释放通过重写AQS的tryRelease方法,源码为:

protected final boolean tryRelease(int releases) {
   if (!isHeldExclusively())
       throw new IllegalMonitorStateException();
 //1. 同步状态减去写状态
   int nextc = getState() - releases;
 //2. 当前写状态是否为0,为0则释放写锁
   boolean free = exclusiveCount(nextc) == 0;
   if (free)
       setExclusiveOwnerThread();
 //3. 不为0则更新同步状态
   setState(nextc);
   return free;
}


源码的实现逻辑请看注释,不难理解与ReentrantLock基本一致,这里需要注意的是,减少写状态int nextc = getState() - releases;只需要用当前同步状态直接减去写状态的原因正是我们刚才所说的写状态是由同步状态的低16位表示的。


3.读锁详解

3.1.读锁的获取

看完了写锁,现在来看看读锁,读锁不是独占式锁,即同一时刻该锁可以被多个读线程获取也就是一种共享式锁。按照之前对AQS介绍,实现共享式同步组件的同步语义需要通过重写AQS的tryAcquireShared方法和tryReleaseShared方法。读锁的获取实现方法为:

protected final int tryAcquireShared(int unused) {
   /*
    * Walkthrough:
    * 1. If write lock held by another thread, fail.
    * 2. Otherwise, this thread is eligible for
    *    lock wrt state, so ask if it should block
    *    because of queue policy. If not, try
    *    to grant by CASing state and updating count.
    *    Note that step does not check for reentrant
    *    acquires, which is postponed to full version
    *    to avoid having to check hold count in
    *    the more typical non-reentrant case.
    * 3. If step 2 fails either because thread
    *    apparently not eligible or CAS fails or count
    *    saturated, chain to version with full retry loop.
    */

   Thread current = Thread.currentThread();
   int c = getState();
 //1. 如果写锁已经被获取并且获取写锁的线程不是当前线程的话,当前
 // 线程获取读锁失败返回-1
   if (exclusiveCount(c) != 0 &&
       getExclusiveOwnerThread() != current)
       return -1;
   int r = sharedCount(c);
   if (!readerShouldBlock() &&
       r < MAX_COUNT &&
   //2. 当前线程获取读锁
       compareAndSetState(c, c + SHARED_UNIT)) {
   //3. 下面的代码主要是新增的一些功能,比如getReadHoldCount()方法
   //返回当前获取读锁的次数
       if (r == 0) {
           firstReader = current;
           firstReaderHoldCount = 1;
       } else if (firstReader == current) {
           firstReaderHoldCount++;
       } else {
           HoldCounter rh = cachedHoldCounter;
           if (rh == || rh.tid != getThreadId(current))
               cachedHoldCounter = rh = readHolds.get();
           else if (rh.count == 0)
               readHolds.set(rh);
           rh.count++;
       }
       return 1;
   }
 //4. 处理在第二步中CAS操作失败的自旋已经实现重入性
   return fullTryAcquireShared(current);
}


代码的逻辑请看注释,需要注意的是  当写锁被其他线程获取后,读锁获取失败,否则获取成功利用CAS更新同步状态。


另外,当前同步状态需要加上SHARED_UNIT((1 << SHARED_SHIFT)即0x00010000)的原因这是我们在上面所说的同步状态的高16位用来表示读锁被获取的次数。如果CAS失败或者已经获取读锁的线程再次获取读锁时,是靠fullTryAcquireShared方法实现的,这段代码就不展开说了,有兴趣可以看看。


3.2.读锁的释放

读锁释放的实现主要通过方法tryReleaseShared,源码如下,主要逻辑请看注释:

protected final boolean tryReleaseShared(int unused) {
   Thread current = Thread.currentThread();
 // 前面还是为了实现getReadHoldCount等新功能
   if (firstReader == current) {
       // assert firstReaderHoldCount > 0;
       if (firstReaderHoldCount == 1)
           firstReader = ;
       else
           firstReaderHoldCount--;
   } else {
       HoldCounter rh = cachedHoldCounter;
       if (rh == || 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;
   }
}


4.锁降级

读写锁支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,不支持锁升级,关于锁降级下面的示例代码摘自ReentrantWriteReadLock源码中:

void processCachedData() {
       rwl.readLock().lock();
       if (!cacheValid) {
           // Must release read lock before acquiring write lock
           rwl.readLock().unlock();
           rwl.writeLock().lock();
           try {
               // Recheck state because another thread might have
               // acquired write lock and changed state before we did.
               if (!cacheValid) {
                   data = ...
           cacheValid = true;
         }
         // Downgrade by acquiring read lock before releasing write lock
         rwl.readLock().lock();
       } finally {
         rwl.writeLock().unlock(); // Unlock write, still hold read
       }
     }

     try {
       use(data);
     } finally {
       rwl.readLock().unlock();
     }
   }
}


【Java知音】实时干货推荐栏

Java并发专题【11】深入理解读写锁ReentrantReadWriteLock

通俗易懂读写锁ReentrantReadWriteLock的使用 ReentrantReadWriteLock不知道大家熟悉吗?其实在实际的项目中用的比较少,反正我所在的项目没有用到过。ReentrantReadWriteLock称为读写锁,它提供一个读锁,支持多个线程共享同一把锁。它也提供了一把写锁,是独占锁,和其他读锁或者写锁互斥,表明只有一个线程能持有锁资源。通过两把锁的协同工作,能够最大化的提高读写的性能,特别是读多写少的场景,而往往大部分的场景都是读多写少的。本文主要讲解ReentrantReadWriteLock的使用和应用场景。 阅读详情

相关推荐

华为产品安全基线

一、敏感信息安全传输 1、采用HTTPS安全协议传输 2、客户端本地保存的敏感信息,需要先加密再保存使用,不可逆支持PBKDF2(迭代10000次)加密保存,可逆支持AES256+IV保存,密钥支持可更新 二、认证鉴权 1、License中心与分析节点分布式部署,需要做用户名+口令安全认证,支持使用HTTPS安全协议。 2、客户端登录支持防暴力破解,支持验证码,多次连续尝试登陆失败后锁定账...

zileixu的博客 1万+

ReentrantReadWriteLock读写锁

一、前言 ReentrantReadWriteLock中包含三种锁:读写锁、可重入锁、公平/非公平锁 1、读写锁 2、可重入锁: 又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。说的有点抽象,下面会有一个代码的示例。 对于Java ReetrantLock而言,从名字就可以看出是一个重入锁,其名字是Re entrant Lock 重新进入锁。 对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。 syn...

m0_56054652的博客 4360

ADS 2024实战:手把手教你搞定CGH40010F的Doherty功放仿真与版图(附避坑指南)

本文详细介绍了使用ADS 2024进行CGH40010F Doherty功放从仿真到版图的全流程设计,涵盖器件建模、稳定性分析、负载牵引匹配和版图联合仿真等关键步骤。特别针对新手常见问题提供避坑指南,帮助工程师高效完成射频功放设计,提升5G基站和雷达系统的性能。

weixin_42528191的博客 221

并发编程之ReentrantReadWriteLock详解

ReentrantReadWriteLock是Java中的一个读写锁实现,它允许多个线程同时读取共享资源(读读可以并发),但在写入时只允许一个线程独占(读写,写读,写写互斥)。这个锁支持重入,即同一个线程可以多次获取相同类型的锁。这种锁可以提高读操作的并发性,同时保证写操作的独占性。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。线程进入读锁的前提条件没有其他线程的写锁没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。

尽力而为! 1943

RealSense D435i深度相机实战:从像素坐标到三维空间的精准映射(Python实现)

本文详细介绍了使用RealSense D435i深度相机实现从二维像素坐标到三维空间坐标精准映射的两种Python实战方法:深度图反投影法与点云直接查找法。文章涵盖环境搭建、相机对齐、代码实现、性能对比及与YOLO等检测模型结合的进阶应用,并提供了关键的避坑指南和参数调优建议,帮助开发者在机器人、三维重建等项目中高效应用。

weixin_29291573的博客 382

链接文件及功能安全:S32K3如何将FLASH驱动放到RAM里面、RAM如何实现软件复位数据不丢失操作。

在通过RTD的SDK也好MCAL也好,始终存在一个问题,生成的代码除了看门狗模块,默认都是放在flash里面,按照正常逻辑来看没啥大的问题,思考一个问题:Flash放在0x100的位置,但是此时我们要操作0x100怎么办?那肯定不允许啊,但是将flash的驱动放在RAM里面就可以避免这个问题的产生。Flash的特性:PF0能操作PF1,当然PF1也能操作PF0的,以此类推,只要不操作自身,想怎么玩都可以。这也是一个物理特性吧。

梦想技术家 2310

ReentrantReadWriteLock原理详解

当读操作远远高于写操作时,这时候使用读写锁让读——读可以并发,提高性能。 类似于数据库中的select.,from..lockinsharemode 提供一个数据容器类内部分别使用读锁保护数据的read()方法,写锁保护数据的write()方法 简单来说,就是读锁可以共享,但是写锁必须独占 一、ReentrantReadWriteLock JUC为我们提供了一个可用于读和写的读写锁。 简单看下使用方法: ReentrantReadWriteLo...

m0_53127809的博客 3469

【QT开发教程】Qt中的跨平台文件系统操作

文件系统操作是应用程序开发中经常需要处理的任务。Qt提供了一套跨平台的文件系统操作API,使开发者能够在不同操作系统上无缝地进行文件和目录操作。本文将介绍Qt中的跨平台文件系统操作,包括基本概念、常用类和方法以及实际示例。

科技改变人类,技术成就未来 981

深入理解Java并发读写锁——ReentrantReadWriteLock

若当前线程不是第一个读线程,那么首先会获取缓存计数器(上一个读锁线程对应的计数器 ),若计数器为空或者tid不等于当前线程的tid值,则获取当前线程的计数器,如果计数器的计数count小于等于1,则移除当前线程对应的计数器,如果计数器的计数count小于等于0,则抛出异常,之后再减少计数即可。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。若存在,则需要被阻塞,否则,无需阻塞),之后在设置状态state,然后返回true。

sdgfafg_25的博客 978

【OD统一考试(B卷)】比赛的冠亚季军,Python 解答

比赛的规则是 0 号和 1 号比赛,2 号和 3 号比赛,以此类推,每一轮,相邻的运动员进行比赛,获胜的进入下一轮,实力值大的获胜,实力值相等的情况,id 小的情况下获胜,轮空的直接进入下一轮。第一轮比赛,id 为 0 实力值为 2 的运动员和 id 为 1 实力值为 3 的运动员比赛, 1 号胜出进入下一轮争夺冠亚军,id 为 2 的运动员和 id 为 3 的运动员比赛,3 号胜出进入下一轮争夺冠亚军;2 号与 0 号,比赛进行季军的争夺,2 号实力值为 4,0 号实力值 2,故 2 号胜出,得季军。

梦想橡皮擦,专栏100例写作模式先行者,现象级专栏 《Python 爬虫 100 例》作者、《滚雪球学 Python 专栏》原创者 1万+

32、机械臂数字孪生模型创建指南

本文介绍了如何在MATLAB Simscape中为一款3自由度(DOF)微型机械臂创建数字孪生模型。内容涵盖硬件参数测量、Simulink模型搭建流程,包括初始条件设置、基座与上部主体构建、上臂结构设计等关键步骤。通过使用Solid、Revolute Joint和Rigid Transform等模块,实现机械臂多体系统建模,并支持后续运动仿真与物理系统的对比诊断。读者可依据指南逐步完成模型构建,相关资源可通过MATLAB File Exchange或指定网站获取。

qsc9012345的博客 215

深入理解读写锁ReentrantReadWriteLock

读写锁ReentrantReadWriteLock用法详解

. 4284

【推荐100个unity插件之8】实现多人在线联机游戏——Mirror插件的使用介绍(附项目源码)

基于Mirror的Unity多人游戏开发指南 本文介绍了使用Mirror插件开发Unity多人游戏的基本流程。Mirror是Unity的高级网络API,替代了已废弃的UNet系统。文章详细讲解了如何导入Mirror插件,创建NetworkManager网络管理器,处理常见错误,以及创建玩家角色和出生点。 主要内容包括: 创建场景NetworkManager并配置必要组件 为玩家添加NetworkIdentity和NetworkTransform组件 设置玩家初始生成位置和生成方式 常见错误及解决方法 通过M

向宇的博客,专注php/web全栈 unity游戏开发,欢迎大家评论纠错 1万+

【Java并发】揭秘Lock体系 -- 深入理解ReentrantReadWriteLock

【Java并发】揭秘Lock体系 -- 深入理解ReentrantReadWriteLock

2403_87459748的博客 1568

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

深入理解Java并发读写锁—ReentrantReadWriteLock

若当前线程不是第一个读线程,那么首先会获取缓存计数器(上一个读锁线程对应的计数器 ),若计数器为空或者tid不等于当前线程的tid值,则获取当前线程的计数器,如果计数器的计数count小于等于1,则移除当前线程对应的计数器,如果计数器的计数count小于等于0,则抛出异常,之后再减少计数即可。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。若存在,则需要被阻塞,否则,无需阻塞),之后在设置状态state,然后返回true。

SevenCoding的博客 1335

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

七:深入理解 ReentrantReadWriteLock —— 读写锁

`ReentrantReadWriteLock`:读写锁。它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁。适用于**读多写少**的场景(如果用独占锁,效率及其低下),提高并发性能,但也需要更多的内存和处理器时间来维护状态信息

sco5282的博客 928

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

Java并发系列(11)深入理解读写锁ReentrantReadWriteLock

本站小福利 点我获取阿里云优惠券 原文作者:github:CL0610/Java-concurrency 免责声明: 1.本文所转载文章均来自公开网络。 2.如果出处标注有误或侵犯到原著作者权益,请联系删除。 3.转载文章请注明原文链接和作者,否则产生的任何版权纠纷均与本站无关。 1.读写锁的介绍 在并发场景中用于解决线程安全的问题,我们几乎会高频率的使用到独占式锁,通常使用java提供的关键字s...

不想当码农的程序员 325

Java 并发编程:深入理解 ReadWriteLock 读写锁

读锁(共享锁):允许多个线程同时获取和持有写锁(排他锁):只允许一个线程获取和持有这种设计特别适合 "读多写少" 的场景,能在保证线程安全的同时显著提升并发性能。ReadWriteLock 通过区分读写操作,在保证线程安全的同时,极大地提高了读多写少场景下的并发性能。它是 Java 并发包中一个非常实用的工具,合理使用能显著提升系统性能。与普通的 synchronized 关键字或 ReentrantLock 相比,ReadWriteLock 为特定场景提供了更精细的控制和更高的并发度。

优快云_xysf的博客 786
上一篇: JAVA并发十:ReentrantLock
下一篇: JAVA并发一:Java 并发编程简介
SATTSA
博客等级 码龄8年 9粉丝 13原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值