前言

 在了解读写锁时,我们首先要明白什么是排他锁?在另个博客写有关于重入锁的测试,关于重入锁这些其实都属于排他锁,什么是排他锁呢?这种锁呢在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
 除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化读写交互场景的编程方式。假设在程序中定义了一个共享的用作缓存数据结构,它大部分时间提供读服务(例如查询和搜索),而写操作占有的时间很少,但是写操作完成之后的更新需要对后续的读服务可见。
 在没有读写锁支持的(java5之前)时候,如果需要完成上述工作就要使用Java的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行(写操作之间依靠synchronized关键字进行同步),这样做的目的是使读操作能读取到正确的数据,不会出现脏读。改用读写锁实现上述功能,只需要在读操作时获取读锁,写操作时获取写锁即可。当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用等待通知机制的实现方式而言,变得简单明了(一句话:读的时候用读锁,写的时候用写锁)。
 一般情况下,读写锁的性能都会比排他锁好,因为大多数场景读是多与写的,在读多与写的情况下,读写锁能够提供比排他锁更好的并发量和吞吐量。Java并发包提供读写锁的实现是ReentrantReadWriteLock。

读写锁的特性

特性 说明
公平性选择 支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
重进入 该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后
锁降级 遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁

读写锁的接口与示例

 ReadWriteLock仅定义了获取读锁和写锁的两个方法,即readLock()方法和writeLock()方法,而其实现---------ReentrantReadWriteLock,除了接口方法之外,还提供了一些便于外界监控其内部工作状态的方法,这些方法以为描述如下

方法名称 描述
int getReadLockCount() 返回当前读锁被获取的次数,该次数不等于获取读锁的线程数,例如,仅一个线程,它连续获取(重进入)了n次读锁,那么占据读锁的线程数是1,但该方法返回n
int getReadHoldCount() 返回当前线程获取读锁的次数,该方法在jdk6中加入到ReentrantReadWriteLock中,使用ThreadLocal保存当前线程获取的次数,这也使得Java6的实现变得更加复杂
boolean isWriteLocked() 判断写锁是否被获取
int getWriteHoldCount() 返回当前写锁被获取的次数

代码示例1

缓存使用读写锁案例

package readwritelock;import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;/*** @author liusupeng* @date 2021/7/21 23:16*/
public class Cache {static Map<String, Object> map = new HashMap<>();static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();static Lock w = rwl.writeLock();static Lock r = rwl.readLock();//获取一个key对应的valuepublic static final Object get(String key) {r.lock();try {return map.get(key);} finally {r.unlock();}}//设置key对应的value,并返回旧的valuepublic static final Object put(String key, Object value) {w.lock();try {return map.put(key, value);} finally {w.unlock();}}//清空所有的内容public static final void clear(){w.lock();try {map.clear();}finally {w.unlock();}}}

上述示例中,Cache组合一个非线程安全的HashMap作为缓存的实现,同时使用读写锁的读锁和写锁来保证Cache是线程安全的。在读操作get(String key)方法中,需要获取读锁,这使得并发访问该方法时不会被阻塞。写操作Put(String key,Object value)方法和clear()方法,在更新HashMap时必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞,而只有写锁被释放之后,其他读写操作才能继续。Cache使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性,同时简化了编程方式。

读写锁的实现分析

 接下来分析ReentrantReadWriteLock的实现,主要包括:读写状态的设计、写锁的获取与释放以及锁降级(以下没有特别说明读写锁均可认为是ReentrantReadWriteLock).
1.读写状态的设计
 读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。回想ReentrantLock中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。
2.写锁的获取与释放
 写锁是一个支持重进入的排他锁。如果当前线程已经获取了写锁,则添加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态,获取写锁的代码如下

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();int c = getState();int w = exclusiveCount(c);if (c != 0) {// (Note: if c != 0 and w == 0 then shared count != 0)if (w == 0 || current != getExclusiveOwnerThread())return false;if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");// Reentrant acquiresetState(c + acquires);return true;}if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))return false;setExclusiveOwnerThread(current);return true;}

 该方法除了重入条件(当前线程为获取了写锁的过程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。
 写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。
3.读锁的获取与释放
 读锁是一个支持冲进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功的获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。获取读锁的实现从java5到java6变得复杂许多,主要原因是新增了一些功能,例如getReadHoldCount()方法,作用是返回当前线程获取读锁的次数。读状态是所有线程获取读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由线程自身维护,这使获取读锁的实现变得复杂。因此,这里将获取读锁的代码做了删减,保留必要的部分。

 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();if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current)return -1;int r = sharedCount(c);if (!readerShouldBlock() &&r < MAX_COUNT &&compareAndSetState(c, c + SHARED_UNIT)) {if (r == 0) {firstReader = current;firstReaderHoldCount = 1;} else if (firstReader == current) {firstReaderHoldCount++;} else {HoldCounter rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current))cachedHoldCounter = rh = readHolds.get();else if (rh.count == 0)readHolds.set(rh);rh.count++;}return 1;}return fullTryAcquireShared(current);}final int fullTryAcquireShared(Thread current) {/** This code is in part redundant with that in* tryAcquireShared but is simpler overall by not* complicating tryAcquireShared with interactions between* retries and lazily reading hold counts.*/HoldCounter rh = null;for (;;) {int c = getState();if (exclusiveCount(c) != 0) {if (getExclusiveOwnerThread() != current)return -1;// else we hold the exclusive lock; blocking here// would cause deadlock.} else if (readerShouldBlock()) {// Make sure we're not acquiring read lock reentrantlyif (firstReader == current) {// assert firstReaderHoldCount > 0;} else {if (rh == null) {rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current)) {rh = readHolds.get();if (rh.count == 0)readHolds.remove();}}if (rh.count == 0)return -1;}}if (sharedCount(c) == MAX_COUNT)throw new Error("Maximum lock count exceeded");if (compareAndSetState(c, c + SHARED_UNIT)) {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; // cache for release}return 1;}}}

在该方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。
 读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少值(1<<16)
锁降级
 锁降级指的是写锁降级成为读锁.如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级.锁降级是指把持住(当前拥有的)写锁,在获取到读锁,随后释放(先前拥有的)写锁的过程.
 接下来看一个锁降级的示例,因为数据不常变化,所以多个线程可以并发的进行数据处理,当数据变更后,如果当前线程感知到数据变化,则进行数据的准备工作,同时其他处理线程被阻塞,知道当前线程完成数据的准备工作.

读写锁(ReentrantReadWriteLock)相关推荐

  1. Java Review - 并发编程_读写锁ReentrantReadWriteLock的原理源码剖析

    文章目录 ReentrantLock VS ReentrantReadWriteLock 类图结构 非公平的读写锁实现 写锁的获取与释放 void lock() void lockInterrupti ...

  2. 并发编程-19AQS同步组件之重入锁ReentrantLock、 读写锁ReentrantReadWriteLock、Condition

    文章目录 J.U.C脑图 ReentrantLock概述 ReentrantLock 常用方法 synchronized 和 ReentrantLock的比较 ReentrantLock示例 读写锁R ...

  3. java 可重入读写锁 ReentrantReadWriteLock 详解

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt206 读写锁 ReadWriteLock读写锁维护了一对相关的锁,一个用于只 ...

  4. 深入分析实战可重入读写锁ReentrantReadWriteLock

    文章目录 前言 加锁规则 同步原理 源码解析 实战演示 前言 前面我们学习了可重入锁ReentrantLock,可重入锁是一个排他锁,只要不是当前线程访问加锁资源都不能够进入,只能等待锁的释放.当然, ...

  5. 深入理解读写锁ReentrantReadWriteLock

    深入理解读写锁ReentrantReadWriteLock 前言 业务开发中我们可能涉及到读写操作. 面对写和读,对于数据同步,在使用Lock锁和 synchronized关键字同步数据时候,对于读读 ...

  6. 读写锁ReentrantReadWriteLock:读读共享,读写互斥,写写互斥

    JDK1.5之后,提供了读写锁ReentrantReadWriteLock,读写锁维护了一对锁,一个读锁,一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升.在读多写少的情况下,读写 ...

  7. Oracle java官网关于可重入读写锁ReentrantReadWriteLock的解析

    Oracle java官网关于可重入读写锁ReentrantReadWriteLock的解析 1.[原文链接](https://docs.oracle.com/javase/8/docs/api/ja ...

  8. jdk读写锁ReentrantReadWriteLock

    public ReentrantReadWriteLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();reader ...

  9. 读写锁 -- ReentrantReadWriteLock

    1. 类继承关系 如图所示, ReadWriteLock 是一个接口,内部由两个Lock 接口组成. public interface ReadWriteLock {Lock readLock();L ...

  10. 读写锁ReentrantReadWriteLock源码分析

    文章目录 读写锁的介绍 写锁详解 写锁的获取 写锁的释放 读锁详解 读锁的获取 读锁的释放 锁降级 读写锁的介绍 在并发场景中用于解决线程安全的问题,我们几乎会高频率的使用到独占式锁,通常使用java ...

最新文章

  1. pytorch笔记:torch.nn.MaxPool2d
  2. SIFT(Scale-invariant feature transform, 尺度不变特征转换)特征
  3. 透明状态栏导致windowSoftInputMode:adjustResize失效问题
  4. 最新版本_adt-bundle-windows-x86_64-20140702 无法建立avd
  5. PID控制的输入量与输出量的关系
  6. 学习MiniGui之多线程机制【转】
  7. 设计模式:给你生产一个女朋友---工厂方法模式
  8. linux service start|stop|restart
  9. 十进制转化八进制,十六进制
  10. 程序猿应该拥有的浏览器
  11. STM32使用HAL库驱动W5500
  12. git 添加文件覆盖文件, 删除文件
  13. 基于WKT标准的空间参考系字符串及prj文件生成样例
  14. 批量生成二维码、打印
  15. 英语四级和计算机一级算多少学分,英语四级成绩怎么算分 多少分合格
  16. 学霸题 - 数正方形
  17. Unbuntu20.04环境下一款开源翻译软件:goldendict的安装与配置(图文)
  18. 2020年教师资格证考试课件百度云网盘地址分享
  19. Vue+Element UI完成新建调查问卷
  20. Unable to restart cluster, will reset it: apiserver healthz: apiserver process never appeared

热门文章

  1. Android_学习系列(33)--App应用之提交到各大市场渠道
  2. 【Android工具】复验谷歌服务安装工具,亲测红米note9安装谷歌服务GooglePlay
  3. android 开发 心电图绘制
  4. 仙剑四,我该为谁而哭—写在上海软星解散之后
  5. 3D全景沉浸式体验场景未来城质量保障方案总结
  6. SourceTree 3x版本跳过登录
  7. hadoop启动主机名解析错误解决
  8. Redis-sentinels启动时报错 All sentinels down, cannot determine where is redis-sentinel master is running.
  9. 数据结构和算法-2023.07.06
  10. IM场景的移动端UI自动化测试平台实践