ReentrantReadWriteLock读写锁

简介: ReentrantReadWriteLock读写锁

用途:

 用于读多写少的场景。

特点:

  •   有一个线程获取到了写锁,则在锁释放前除当前线程外,其他线程都不能获取到读锁或写锁。但当前线程可获取读锁(降级)和写锁(重入)。
  •  有一个线程获取到了读锁,则在锁释放前所有线程只能获取到读锁(当前线程可再次重入读锁),不能获取到写锁(即不支持锁升级)。
  • 读锁不支持条件变量,使用则抛异常。

读写锁的使用

private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock r = readWriteLock.readLock();
private Lock w = readWriteLock.writeLock();  // 读操作上读锁
public Data get(String key) {
    r.lock();
    try {
        // TODO 业务逻辑
    } finally {
        r.unlock();
    }
}
// 写操作上写锁
public Data put(String key, Data value) {
    w.lock();
    try {
        // TODO 业务逻辑
    } finally {
        w.unlock();
    }
}

锁降级

锁降级指的是写锁降级成为读锁。 如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。 锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。 锁降级可以帮助我们拿到当前线程修改后的结果而不被其他线程所破坏,防止更新丢失。

锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性 ,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。

RentrantReadWriteLock不支持锁升级 (把持读锁、获取写锁,最后释放读锁的过程)。目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。

读写状态的设计

设计的精髓:用一个变量如何维护多种状态

在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int 类型的 state 来表示同步状态,表示锁被一个线程重复获取的次数。但是, 读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,如果要用一个变量维护多种状态,需要采用“按位切割使用”的方式来维护这个变量,将其切分为两部分:高16为表示读,低16为表示写。

分割之后,读写锁是如何迅速确定读锁和写锁的状态呢?通过位运算。假如当前同步状态为S,

那么:

  • 写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)。 当写状态加1,等于S+1.
  • 读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。当读状态加1,等于S+(1<<16),也就是S+0x00010000根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。

HoldCounter 计数器

     读锁的内在机制其实就是一个共享锁 。一次共享锁的操作就相当于对HoldCounter 计数器的操作。 获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。 只有当线程获取共享锁后才能对共享锁进行释放、重入操作。

    通过 ThreadLocalHoldCounter 类,HoldCounter 与线程进行绑定。HoldCounter 是绑定线程的一个计数器,而 ThreadLocalHoldCounter 则是线程绑定的ThreadLocal。 HoldCounter是用来记录读锁重入数的对象ThreadLocalHoldCounter是ThreadLocal变量,用来存放不是第一个获取读锁的线程的其他线程的读锁重入数对象

写锁的获取

写锁是一个支持重进入的排它锁。 如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。写锁的获取是通过重写AQS中的tryAcquire方法实现的。

通过源码我们可以知道:

  • 读写互斥
  • 写写互斥
  • 写锁支持同一个线程重入
  • writerShouldBlock写锁是否阻塞实现取决公平与非公平的策略(FairSync和 NonfairSync)

写锁的释放

写锁释放通过重写AQS的tryRelease方法实现

读锁的获取

 实现共享式同步组件的同步语义需要通过重写AQS的tryAcquireShared方法和tryReleaseShared方法。

  • 读锁共享,读读不互斥
  • 读锁可重入,每个获取读锁的线程都会记录对应的重入数
  • 读写互斥,锁降级场景除外
  • 支持锁降级,持有写锁的线程,可以获取读锁,但是后续要记得把读锁和写锁读释放
  • readerShouldBlock读锁是否阻塞实现取决公平与非公平的策略FairSync和NonfairSync)

读锁的释放

获取到读锁,执行完临界区后,要记得释放读锁(如果重入多次要释放对应的次数),不然会

阻塞其他线程的写操作。读锁释放的实现主要通过方法tryReleaseShared。

相关文章
|
存储 安全 Java
ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别?
ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别?
|
存储 SQL 运维
一篇文章彻底理解 HDFS 的安全模式
一篇文章彻底理解 HDFS 的安全模式
|
存储 Kubernetes API
JuiceFS-开源分布式文件系统入门(一篇就够了)(上)
JuiceFS-开源分布式文件系统入门(一篇就够了)(上)
840 0
|
算法 安全 架构师
浅浅瞅瞅RSA-PSS 算法
浅浅瞅瞅RSA-PSS 算法
897 0
|
JSON 前端开发 Java
[Spring~源码] ControllerAdvice揭秘
[Spring~源码] ControllerAdvice揭秘
394 0
|
NoSQL Java 程序员
为什么引入Redisson分布式锁?
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包含了各种各样的分布式锁的实现。
为什么引入Redisson分布式锁?
|
缓存 安全 Java
JAVA的读写锁中,读锁为什么不能升级为写锁?
读写锁,并不是 Java 语言特有的,而是一个广为使用的通用技术,所有的读写锁都遵守以下三条基本原则
|
消息中间件 存储 监控
Apache Kafka-消费端消费重试和死信队列
Apache Kafka-消费端消费重试和死信队列
3741 0
|
消息中间件 存储 运维
Kafka消费组/者协调器的介绍
什么是协调器 协调器是用于协调多个消费者之间能够正确的工作的一个角色, 比如计算消费的分区分配策略,又或者消费者的加入组与离开组的处理逻辑, 有一点类似Kafka种的控制器的角色。
2832 0
Kafka消费组/者协调器的介绍
|
存储 缓存 NoSQL
详解Redisson分布式限流的实现原理
我们目前在工作中遇到一个性能问题,我们有个定时任务需要处理大量的数据,为了提升吞吐量,所以部署了很多台机器,但这个任务在运行前需要从别的服务那拉取大量的数据,随着数据量的增大,如果同时多台机器并发拉取数据,会对下游服务产生非常大的压力。之前已经增加了单机限流,但无法解决问题,因为这个数据任务运行中只有不到10%的时间拉取数据,如果单机限流限制太狠,虽然集群总的请求量控制住了,但任务吞吐量又降下来
736 0