当前位置: 首页 > ds >正文

ReentrantReadWriteLock读写锁

一、锁的分类

这里不会对Java中大部分的分类都聊清楚,主要把 **互斥,共享** 这种分类聊清楚。

Java中的互斥锁,synchronized,ReentrantLock这种都是互斥锁。一个线程持有锁操作时,其他线程都需要等待前面的线程释放锁资源,才能重新尝试竞争这把锁。

Java中的读写锁(支撑互斥&共享),Java中最常见的就是 **ReentrantReadWriteLock** ,StampedLock。

其中StampedLock是JDK1.8中推出的一款读写锁的实现,针对ReentrantReadWriteLock一个优化。但是,今儿不细聊。主要玩ReentrantReadWriteLock。

ReentrantReadWriteLock主要就是解决咱们刚才聊的,读写操作都有,读操作居多,写操作频次相对比较低的情况,可以使用读写锁来提升系统性能。

读写锁中:

* 写写互斥
* 读写互斥
* 写读互斥
* 读读共享
* 有锁降级的情况,后面聊!!

二、ReentrantReadWriteLock的基本操作

ReentrantReadWriteLock中实现了ReadWriteLock的接口,在这个接口里面提供了两个抽象方法。

正常的操作,是new ReentrantReadWriteLock的对象,但是你具体的业务操作是需要读锁,还是写锁,你需要单独的获取到,然后针对性的加锁。

public interface ReadWriteLock {/*** Returns the lock used for reading.** @return the lock used for reading*/Lock readLock();/*** Returns the lock used for writing.** @return the lock used for writing*/Lock writeLock();
}

具体使用方式

public static void main(String[] args){// 1、构建读写锁对象ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();// 2、单独获取读、写锁对象ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();// 3、根据业务使用具体的锁对象加锁writeLock.lock();// try-finally的目的,是为了避免没有及时释放锁资源导致死锁的问题。try{// 4、业务操作…………System.out.println("写操作");}finally {// 5、释放锁writeLock.unlock();}
}

三、ReentrantReadWriteLock的底层实现

ReentrantReadWriteLock是基于AQS实现的。

AQS是JUC包下的一个抽象类AbstractQueuedSynchronizer

暂时只关注两点,分别是AQS提供的state属性,还有AQS提供的一个同步队列。

state属性,用来标识当前 读写锁 的资源是否被占用的核心标识。
private volatile int state;

一个int类型的state,是4字节,每个字节占用8个bit位,一个state占用32个bit位。

* 高16位,作为读锁的标记。
* 低16位,作为写锁的标记。

static final int SHARED_SHIFT   = 16;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
00000000 00000000 11111111 11111111
/** 查看读锁的占用情况。 */
static int sharedCount(int state)    { return state >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count  */
static int exclusiveCount(int state) { return state & EXCLUSIVE_MASK; }00000000 00000000 00000000 00000000    int类型的数值的32个bit位。 读锁占用情况:
00000000 00000011 00000000 00000000    state
>>> 16 
00000000 00000000 00000000 00000011    读锁被获取了三次。写锁占用情况。(这里之所以&这个么东西,是对后期的锁降级有影响~)
00000000 00000000 00000000 00000001    state
&
00000000 00000000 11111111 11111111  
=
00000000 00000000 00000000 00000001    写锁被获取了一次。

一个同步队列,当线程获取锁资源失败时,需要到这个同步队列中排队。到了合适的时机,就会继续尝试获取对应的锁资源。

四、ReentrantReadWriteLock的锁重入

同一个线程,多次获取同一把锁时,就会出现锁重入的情况。

而咱们大多数的锁,都会提供锁重入的功能。

锁重入场景:

public class Demo {// 1、构建读写锁对象static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();// 2、单独获取读、写锁对象static ReentrantReadWriteLock.ReadLock readLock;static ReentrantReadWriteLock.WriteLock writeLock;static{// 2、单独获取读、写锁对象readLock = readWriteLock.readLock();writeLock = readWriteLock.writeLock();}public static void main(String[] args){// 3、根据业务使用具体的锁对象加锁writeLock.lock();// try-finally的目的,是为了避免没有及时释放锁资源导致死锁的问题。try{// 4、业务操作…………调用其他方法xxx();}finally {// 5、释放锁writeLock.unlock();}}private static void xxx(){writeLock.lock();try{// 其他按业务}finally {writeLock.unlock();}}
}

咱们底层的锁重入逻辑很简单

**写锁:** 写锁的实现就是每一次获取写锁时,会对state的低16位+1,再次获取,再次+1。同理,每次释放锁资源时,也需要对state进行-1。 而当对state的低16位减到0时,锁资源就释放干净了。

读锁: 首先,读锁是共享的,他用state的高16位来维护信息。如果高16位的state的值,经过运算,知道了是4,也就是读锁被获取了4次。可能A线程获取了2次读锁资源。 B线程获取了2次读锁资源。高位的state自然就是4。但是因为程序员写代码除了问题,使用A线程,释放了4次读锁资源,那此时B线程是不是就可能出现数据安全问题了。

所以,为了解决上述的问题,每个线程需要独立的记录自己获取了几次读锁资源。可以使用ThreadLocal来保存线程局部的信息,每次加锁时,ThreadLocal中需要存储一个标记,每次+1。每次释放锁时,也需要将ThreadLocal中的标记进行-1。读线程最后是基于自己的ThreadLocal中的数值,来确认读锁是否释放干净。

 

五、ReentrantReadWriteLock的写锁饥饿

写锁饥饿的问题。

如果写线程在AQS中排队,并且排在head.next的位置。 那么其他想获取读锁的读线程需要排队。避免大量的读请求获取读锁,让写线程一直AQS队列中排队,无法执行写操作的问题。

通过源码可以看到,读写锁中,仅仅针对head.next这个节点的情况,来确认读线程获取读锁时是否需要排队

// 这个方法,总结一句话。  
// AQS中有排队的Node,并且head的next节点是一个有线程并且在等待写锁的Node
final boolean apparentlyFirstQueuedIsExclusive() {Node h, s;return (h = head) != null &&(s = h.next)  != null &&!s.isShared()         &&s.thread != null;
}

ReentrantReadWriteLock读写锁中有锁降级,但是这个和synchronized的锁升级没任何关系!!!

六、ReentrantReadWriteLock的锁降级

ReentrantReadWriteLock的锁降级是指当前线程如果持有了写锁,可以降级直接获取到读锁。

在读写锁中,持有写锁的同时,再去获取读锁,这种行为一般被称为 **锁降级** 。

在读写锁中,持有读锁的同时,去获取写锁,这种行为被称为 **锁升级** ,这个行为是不允许的。

这里是获取读锁的的逻辑,看一下锁降级的支持方式

// 竞争读锁。
if (exclusiveCount(c) != 0 &&   // 这行代表某个线程持有写锁
    getExclusiveOwnerThread() != current)    // 这行代表持有写锁的不是当前线程
    // 退出竞争,无法获取读锁
    return -1;  

前面逻辑没有走return - 1之后,在后续就会正常的对state的高位+1,并且完成读锁的计数操作。

七、ReentrantReadWriteLock的优化 

ReentrantReadWriteLock的优化主要是在读锁计数层面上做的优化。

这个对性能的优化微乎其微,但是确确实实是一个优化。

在获取读锁时,因为是共享的,这种优化只针对第一个获取读锁的线程和最后一个获取读锁的线程。

针对第一个获取读锁的线程,他采用一个全局变量记录重入次数。这个操作可以节省掉使用ThreadLocal的时间成本和内存成本。

其中firstReader记录第一个获取读锁的线程。

firstReaderHoldCount,记录第一个获取读锁的线程的重入次数。

这里是最后一个获取读锁的线程需要走的逻辑

cachedHoldCounter这个属性是记录最后一个获取读锁的线程的重入次数。

这里可以让最后一个获取读锁的线程在重入时,省略掉去ThreadLocal中get计数器的操作,但是之前的set存储操作,不能省略

// 获取上次最后获取读锁的线程
HoldCounter rh = cachedHoldCounter;
// 查看当前线程是否是之前的cachedHoldCounter
if (rh == null || rh.tid != getThreadId(current))
    // 说明不是,将当前获取读锁的线程设置为cachedHoldCounter
    cachedHoldCounter = rh = readHolds.get();
// 这个判断代表第一次获取读锁才会进去
else if (rh.count == 0)
    // 如果是第一次获取读锁,不是重入,还是需要扔到ThreadLocal里纪录好,。
    readHolds.set(rh);
// 直接对获取到的rh做++操作,代表获取了一次读锁。
rh.count++;

 

http://www.xdnf.cn/news/609.html

相关文章:

  • Nacos配置中心客户端处理服务端配置信息源码解析
  • 透视部分国产碳化硅(SiC)MOSFET栅氧可靠性报告造假乱象
  • ClickHouse核心架构设计
  • 【中间件】redis使用
  • Gen - CDPT举例说明:动态上下文前缀(输入先和标签结合,输出结果会更贴近标签内容)
  • 大数据可能出现的bug之flume
  • Matlab 汽车行驶速度PID控制系统仿真
  • GIS开发笔记(9)结合osg及osgEarth实现三维球经纬网格绘制及显隐
  • 利用 HEMT 和 PHEMT 改善无线通信电路中的增益、速度和噪声
  • 常见的页面报错
  • 热门算法面试题第19天|Leetcode39. 组合总和40.组合总和II131.分割回文串
  • windows docker desktop 无法访问容器端口映射
  • 大模型面经 | 介绍一下CLIP和BLIP
  • mac监控linux上mysql性能(Grafana+Prometheus+mysqld_exporter)
  • 每日一题——最小测试用例集覆盖问题
  • 通过爬虫方式实现头条号发布视频(2025年4月)
  • 2025 UCSCCTF Pwn-wp(含附件)
  • Java链表反转方法详解
  • 2. 什么是最普通的自动化“裸奔状态”?
  • 扣子智能体1:创建Agent与写好提示词
  • 深入理解Linux中的线程控制:多线程编程的实战技巧
  • 【失败总结】Win10系统安装docker
  • C++ MySQL数据库访问工具类设计与操作流程详解
  • 实现AWS Data Pipeline安全地请求企业内部API返回数据
  • 学习笔记二十——Rust trait
  • 网络基础(协议,地址,OSI模型、Socket编程......)
  • C++ 多态
  • 支持向量机(SVM):原理、应用与深入解析
  • 【今日三题】判断是不是平衡二叉树(递归) / 最大子矩阵(二维前缀和) / 小葱的01串(滑动窗口)
  • Linux进程地址空间、写时拷贝