6.JUC之ReentrantReadWriteLock

一、概述:

  Java纪年1.5年,ReentrantReadWriteLock诞生于JUC,此后,国人一般称它为读写锁。人如其名,他就是一个可重入锁,同时他还是一个读写锁

  a)跟ReentrantLock并没有任何的亲属关系

  因为ReentrantReadWriteLock在命名上跟ReentrantLock非常贴近,很容易让人认为他跟ReentrantLock有继承关系,其实并没有。ReentrantReadWriteLock 实现了 ReadWriteLock 和 Serializable,同时 ReadWriteLock 跟 Lock 也没有继承关系

  ReadWriteLock 是独立的一个接口,维护了一对相关的,一个用于只读操作,另一个用于写入操作,只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。

  ReentrantReadWriteLock 跟 ReentrantLock 只有朋友关系,他们都是 可重入锁

  但是ReentrantReadWriteLock 的重入递归层级只有 65535,即读锁能递归65535、写锁也同样能够递归65535层,至于为何是65535呢?在AQS框架的时候说过,AQS是用一个Integer来表示锁的状态。而一个Integer有32位,读锁用一半,写锁用一半,16bit = 65535

  

  b)ReentrantReadWriteLock也有公平性

  ReentrantReadWriteLock除了和ReentrantLock一样具有可重入性之外,他们也都具有公平性。既他们都有公平锁和非公平锁的实现。实现方式也差不太远,关于公平性的内容可以查看上一篇博客

二、ReentrantReadWriteLock中的读锁与写锁

  ReentrantReadWriteLock 提供一个读写分离的锁,读锁由ReadLock控制,写锁由WriteLock完成。当然读与写是互斥的。如你所知,可读不写,可写不读,即是读写不能同时进行,这就是读写锁

。之所以能做到读写互斥说明他们最终还是用了同一个同步器(Sync),他们依赖于上层(ReentrantReadWriteLock)的同步器,Sync只有一个,所以读锁与写锁不能同时使用

通过查看源码,我们可以得到ReentrantReadWriteLock的以下几个特点:

  1.当读锁被持有,不管是被一人持有,还是多人持有,写都需要阻塞。

  2.当写锁被持有,当然只有一人能持有(独占), 读锁将会被阻塞

  3.读写锁的阻塞方式直接由公平性决定,由FairSync  或  NonFairSync实现

   4.读锁可以有多人同时持有,HoldCounter的作用就是当前线程持有共享锁的数量

ReentrantReadWriteLock  中的 WriteLock写锁的获取和释放过程和 ReentrantLock 几乎相同,都是独占排他锁,都是使用了AQS的acquire/release操作

ReadLock读锁就有区别了,前面我们提到 AQS提供了几个抽象方法让子类去实现,其中就有  tryAcquireShared tryReleaseShared,用于共享锁的获取和释放,所以我们这里重点关注下 读锁的获取和释放过程:

  读取的获取

 1 protected final int tryAcquireShared(int unused) {
 2         Thread current = Thread.currentThread();
 3         //锁的持有线程数
 4         int c = getState();
 5         /*
 6          * 如果写锁线程数 != 0 ,且独占锁不是当前线程则返回失败,因为存在锁降级
 7          */
 8         if (exclusiveCount(c) != 0 &&
 9             getExclusiveOwnerThread() != current)
10             return -1;
11         //读锁线程数
12         int r = sharedCount(c);
13         /*
14          * readerShouldBlock():读锁是否需要等待(公平锁原则)
15          * r < MAX_COUNT:持有线程小于最大数(65535)
16          * compareAndSetState(c, c + SHARED_UNIT):设置读取锁状态
17          */
18         if (!readerShouldBlock() && r < MAX_COUNT &&
19             compareAndSetState(c, c + SHARED_UNIT)) {
20             /*
21              * holdCount部分后面讲解
22              */
23             if (r == 0) {
24                 firstReader = current;
25                 firstReaderHoldCount = 1;
26             } else if (firstReader == current) {
27                 firstReaderHoldCount++;        //
28             } else {
29                 HoldCounter rh = cachedHoldCounter;
30                 if (rh == null || rh.tid != current.getId())
31                     cachedHoldCounter = rh = readHolds.get();
32                 else if (rh.count == 0)
33                     readHolds.set(rh);
34                 rh.count++;
35             }
36             return 1;
37         }
38         return fullTryAcquireShared(current);
39     } 

  读锁的获取过程分析:

    1.如果写线程持有锁(也就是独占锁数量不为0),并且独占线程不是当前线程(为什么还要满足这个条件,是为了实现 锁降级),那么读取失败

    2.如果读线程请求锁数量达到了 65535(包括重入的部分)(state),那么就抛出异常

    3.如果读线程不用等待(实际上是 是否需要公平锁),并且增加读取锁状态数成功,那么就返回成功,否则执行下一步

    4.上一步失败的原因是 CAS操作修改状态数失败,那么就需要循环不断尝试去修改状态直到成功 或者 锁被写入线程占有

HoldCounter

  前面我们提到,读锁可以有多人同时持有,HoldCounter的作用就是记录当前线程持有共享锁的数量(不是记录所有读线程的共享锁数量,那个由state去记录),下面我们看看 HoldCounter是什么东西,又是如何完成计数的

  首先我们看到只有在获取共享锁(读锁)的时候 + 1,也只有在释放共享锁的时候 - 1 ,会起作用

  强调一下,对于共享锁,其实并不是锁的概念,更像是计数器的概念。一个共享锁就相对于一个计数器操作,一次获取共享锁相当于计数器 + 1,释放一个共享锁相当于计数器 - 1.显然只有线程持有了共享锁(也就是当前线程携带一个计数器,描述自己持有多少个共享锁或者多重共享锁),才能释放一个共享锁。否则一个没有获取共享锁的线程调用一次释放操作就会导致读写锁的state(持有的线程数,包括重入数)错误

  先看读锁获取锁的部分:

 1 if (r == 0) {        //r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中
 2         firstReader = current;
 3         firstReaderHoldCount = 1;
 4     } else if (firstReader == current) {    //第一个读锁线程重入
 5         firstReaderHoldCount++;
 6     } else {    //非firstReader计数
 7         HoldCounter rh = cachedHoldCounter;        //readHoldCounter缓存
 8         //rh == null 或者 rh.tid != current.getId(),需要获取rh
 9         if (rh == null || rh.tid != current.getId())
10             cachedHoldCounter = rh = readHolds.get();
11         else if (rh.count == 0)
12             readHolds.set(rh);        //加入到readHolds中
13         rh.count++;        //计数+1
14     }  

  HoldCounter的定义:

1 static final class HoldCounter {
2             int count = 0;
3             final long tid = Thread.currentThread().getId();
4         }  

  在HoldCounter中仅有count和tid两个变量,其中count代表着计数器,tid是线程的id。但是如果要将一个对象和线程绑定起来仅记录tid肯定不够的,而且HoldCounter根本不能起到绑定对象的作用,只是记录线程tid而已。

  诚然,在java中,我们知道如果要将一个线程和对象绑定在一起只有ThreadLocal才能实现。所以如下:

1 static final class ThreadLocalHoldCounter
2             extends ThreadLocal<HoldCounter> {
3             public HoldCounter initialValue() {
4                 return new HoldCounter();
5             }
6 }  

  故而,HoldCounter应该就是绑定线程上的一个计数器,而ThradLocalHoldCounter则是线程绑定的ThreadLocal。从上面我们可以看到ThreadLocal将HoldCounter绑定到当前线程上,同时HoldCounter也持有线程Id,这样在释放锁的时候才能知道ReadWriteLock里面缓存的上一个读取线程(cachedHoldCounter)是否是当前线程。这样做的好处是可以减少ThreadLocal.get()的次数,因为这也是一个耗时操作。需要说明的是这样HoldCounter绑定线程id而不绑定线程对象的原因是避免HoldCounter和ThreadLocal互相绑定而GC难以释放它们(尽管GC能够智能的发现这种引用而回收它们,但是这需要一定的代价),所以其实这样做只是为了帮助GC快速回收对象而已

四、总结

  关于ReentrantReadWriteLock里面的内容还有很多,有时间还可以去看看源码去品一品,

  在Java1.8之前,它是JDK实现的读写锁(ReadWriteLock)的唯一实现。他由读、写锁两部分组成,写是独占锁,而读是共享锁,且读写互斥

  ReentrantReadWriteLock和ReentrantLock并无关系,但是他们有很多类似的地方,比如都具有可重入性、都有两种获取锁的策略:公平与非公平,与ReentrantLock一样在非公平模式能获得更高的OPS

时间: 2024-10-31 21:19:08

6.JUC之ReentrantReadWriteLock的相关文章

多线程并发编程总结(二)

本文基于https://github.com/h2pl/Java-Tutorial的总结 ReentrantReadWriteLock(读写锁)源码分析 ReentrantReadWriteLock 分为读锁和写锁两个实例: 读锁是共享锁,可被多个线程同时使用,写锁是独占锁. 持有写锁的线程可以继续获取读锁(锁降级),反之不行(持有读锁必须先释放才能再次获取写锁). AQS 的精髓在于内部的属性 state: 独占模式,通常就是 0 代表可获取锁,>=1 代表锁被别人获取了. 共享模式下,每个线

Java多线程之JUC包:ReentrantReadWriteLock源码学习笔记

若有不正之处请多多谅解,并欢迎批评指正. 请尊重作者劳动成果,转载请标明原文链接: http://www.cnblogs.com/go2sea/p/5634701.html ReentrantLock提供了标准的互斥操作,但在应用中,我们对一个资源的访问有两种方式:读和写,读操作一般不会影响数据的一致性问题.但如果我们使用ReentrantLock,则在需要在读操作的时候也独占锁,这会导致并发效率大大降低.JUC包提供了读写锁ReentrantReadWriteLock,使得读写锁分离,在上述情

【Java并发】JUC—ReentrantReadWriteLock有坑,小心读锁!

载自:https://my.oschina.net/meandme/blog/1839265 好长一段时间前,某些场景需要JUC的读写锁,但在某个时刻内读写线程都报超时预警(长时间无响应),看起来像是锁竞争过程中出现死锁(我猜).经过排查项目并没有能造成死锁的可疑之处,因为业务代码并不复杂(仅仅是一个计算过程),经几番折腾,把注意力转移到JDK源码,正文详细说下ReentrantReadWriteLock的隐藏坑点. 过程大致如下: 若干个读写线程抢占读写锁 读线程手脚快,优先抢占到读锁(其中少

JUC之ReadWriteLock、ReentrantReadWriteLock读写锁

读写锁简介 对共享资源有读和写的操作,且写操作没有读操作那么频繁.在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程读取共享资源:但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写操作了. 读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁:一个是写相关的锁,称为排他锁,描述如下: 读锁的条件: 1. 没有其他线程的写锁: 2. 对写锁请求的线程必须是同一个. 写锁的条件: 1. 没有其他线程的读锁

被面试官吊打系列之JUC之 可重入读写锁ReentrantReadWriteLock 之 源码详尽分析

可重入读写锁 ReentrantReadWriteLock 其实基本上模拟了文件的读写锁操作.ReentrantReadWriteLock 和ReentrantLock 的差别还是蛮大的: 但是也有很多的相似之处: ReentrantReadWriteLock 的 writerLock 其实就是相当于ReentrantLock,但是它提供更多的细腻的控制:理解什么是读锁.写锁非常重要,虽然实际工作中区分读写锁这样的细分使用场景比较少. ReentrantReadWriteLock 把锁进行了细化

ReadWriteLock与ReentrantReadWriteLock

JAVA的JUC包中的锁包括"独占锁"和"共享锁".JUC中的共享锁有:CountDownLatch.CyclicBarrier.Semaphore.ReentrantReadWriteLock等.本章会以ReentrantReadWriteLock为蓝本对共享锁进行说明. 一.ReentrantLock与ReentrantReadWriteLock 说到ReentrantReadWriteLock,首先要做的是与ReentrantLock划清界限.它和后者都是单独

十一、JUC包中的锁

JUC,即java.util.concurrent. 悲观锁和乐观锁 悲观锁和乐观锁是一种思想. 悲观锁,持有一种悲观的态度,认为会出现很坏的情况,所以,先做预防措施.独占锁是一种悲观锁,synchronized就是一种独占锁. 而乐观锁,则是持有一种持有种乐观的态度,认为不会出现什么问题,有问题了再说. 对于常用多线程编程的人估计知道,在jdk5之前,在多线程编程的时候,为了保证多个线程对一个对象同时进行访问时,我们需要加同步锁synchronized,保证对象的在使用时的正确性,但是加锁的机

【目录】JUC锁框架目录

JUC锁框架的目录整理如下: 1. [JUC]JUC锁框架综述 2. [JUC]JDK1.8源码分析之LockSupport(一) 3. [JUC]JDK1.8源码分析之AbstractQueuedSynchronizer(二) 4. [JUC]JDK1.8源码分析之ReentrantLock(三) 5. [JUC]JDK1.8源码分析之CyclicBarrier(四) 6. [JUC]JDK1.8源码分析之CountDownLatch(五) 7. [JUC]JDK1.8源码分析之Semapho

Java多线程系列--“JUC锁”03之 公平锁(一)

基本概念 本章,我们会讲解"线程获取公平锁"的原理:在讲解之前,需要了解几个基本概念.后面的内容,都是基于这些概念的:这些概念可能比较枯燥,但从这些概念中,能窥见"java锁"的一些架构,这对我们了解锁是有帮助的.1. AQS -- 指AbstractQueuedSynchronizer类.    AQS是java中管理"锁"的抽象类,锁的许多公共方法都是在这个类中实现.AQS是独占锁(例如,ReentrantLock)和共享锁(例如,Semap