Java 并发编程(五)读写锁
本文使用的 JDK 版本为 JDK 8
JUC
中关于读写锁的接口定义如下:
// java.util.concurrent.locks.ReadWriteLock
public interface ReadWriteLock {
// 返回一个读锁
Lock readLock();
// 返回一个写锁
Lock writeLock();
}
在 JUC
中,常用的具体实现为 ReentrantReadWriteLock
,因此,在这里以 ReentrantReadWriteLock
为例来介绍读写锁的相关内容。
基本使用
读写锁的一个常用的使用场景就是对于数据的读取操作,在大部分的业务场景下,发生读的情况要比发生写的概率要高很多。在这种情况,可以针对热点数据进行缓存,从而提高系统的响应性能。
使用示例如下:
// 该代码来源于 JDK 的官方文档,稍作了一点修改
class CachedData {
Object data;
volatile boolean cacheValid;
final DataAccess access;
final Order order;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
CachedData(DataAccess access, Order order) {
this.access = access;
this.order = order;
}
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) { // 当前缓存已经失效了,即已经发生了写事件
// 在获取写锁之前必须释放读锁,否则会造成死锁
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
/*
重新判断缓存是否是失效的,
因为在这个过程中可能已经有其它的线程对这个缓存的数据进行修改了
*/
if (!cacheValid) {
data = access.queryData();
cacheValid = true;
}
/*
获取读锁,在持有写锁的情况下,可以获得读锁,这也被称为 “锁降级”
*/
rwl.readLock().lock();
} finally {
// 释放写锁,此时依旧持有读锁
rwl.writeLock().unlock();
}
}
try {
order.useData(data);
} finally {
// 注意最后一定要释放锁
rwl.readLock().unlock();
}
}
interface DataAccess {
Object queryData();
}
interface Order {
void useData(Object data);
}
}
源码解析
构造函数
首先,查看 ReentrantReadWriteLock
实例对象的属性
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
// ReentrantReadWriteLock 的静态内部类,为读锁
private final ReentrantReadWriteLock.ReadLock readerLock;
// ReentrantReadWriteLock 的静态内部类,为写锁
private final ReentrantReadWriteLock.WriteLock writerLock;
// 同步工具类,为 AQS 的具体子类
final Sync sync;
public ReentrantReadWriteLock() {
this(false);
}
// 构造函数,初始化 ReentrantReadWriteLock,默认情况选择使用非公平的同步工具
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
}
再查看 ReadLock
和 WriteLock
的相关定义,首先查看 ReadLock
相关的源代码:
public static class ReadLock implements Lock, java.io.Serializable {
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync; // 注意这里的 sync
}
// 省略其它一些不是特别重要的代码
}
再查看 WriteLock
相关的源代码:
public static class WriteLock implements Lock, java.io.Serializable {
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync; // 注意这里的 sync
}
// 省略其它一些不是特别重要的代码
}
可以看到,ReadLock
和 WriteLock
都使用了同一个 Sync
实例对象来维持自身的同步需求,这点很关键
原理
ReadLock
中关于获取锁和释放锁的源代码:
// 获取锁
public void lock() {
sync.acquireShared(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// 释放锁
public void unlock() {
sync.releaseShared(1);
}
WriteLock
中关于获取锁和释放锁的源代码:
// 获取锁
public void lock() {
sync.acquire(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
// 释放锁
public void unlock() {
sync.release(1);
}
通过对比 ReadLock
和 WriteLock
中获取锁和释放锁的源代码,很明显,ReadLock
是以 “共享模式” 的方式获取和释放锁,而 WriteLock
则是通过以独占的方式来获取和释放锁。这两种获取和释放锁的实现都在 AQS
中定义,在此不做过多的详细介绍
再结合上文关于 ReadLock
和 WriteLock
的构造函数,可以发现它们是使用了同一个 AQS
子类实例对象,也就是说,在 ReentrantReadWriteLock
中的 AQS
的具体子类既使用了“共享模式”,也使用了“独占模式”
更一般地来讲,回忆一下 AQS
关于 “共享模式” 和 “独占模式” 对于 state
变量的使用,“共享模式” 将 state
共享,每个线程都能访问 state
;“独占模式” 下,state
被视作是获取到锁的状态,0 表示还没有线程获取该锁,大于 0 则表示线程获取锁的重入次数
为了能够实现 ReentrantReadWriteLock
中的两个模式的共用的功能,ReentrantReadWriteLock
中 Sync
类对 state
进行了如下的处理:
ReentrantReadWriteLock
使用了一个 16 位的状态来表示写入锁的计数,并且使用了另外一个 16 位的状态来表示读锁的计数
就是说,state
变量已经被拆分成了两部分,由于 state
是一个 32 位的整数,现在 state
的前 16 位用于单独处理“共享模式”,而后 16 位则用于处理 “独占模式”
Sync
核心部分就是分析 Sync
的源代码,在这里定义了对 state
变量的修改以及获取锁和释放锁的逻辑
首先查看 Sync
相关字段属性:
abstract static class Sync extends AbstractQueuedSynchronizer {
// 这几个字段的作用就是将 state 划分为两部分,前 16 位为共享模式,后 16 位为独占模式
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// -1 的目的值为了得到最大值
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 取 c 的前 16 位, 只需要右移即可
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 取 c 的后 16 位,只要与对应的掩码按位与即可
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
// 该类的作用是记录每个线程持有的读锁的数量
static final class HoldCounter {
// 线程持有的读锁的数量
int count = 0;
// 线程的 ID
final long tid = getThreadId(Thread.currentThread());
}
// ThreadLocal 的子类
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
/*
用于记录线程持有的读锁信息
*/
private transient ThreadLocalHoldCounter readHolds;
/*
用于缓存,用于记录 “最后一个获取读锁” 的线程的读锁的重入次数
*/
private transient HoldCounter cachedHoldCounter;
/*
第一个获取读锁的线程(并且未释放读锁)
*/
private transient Thread firstReader = null;
// 第一个获取读锁的线程持有的读锁的数量(重入次数)
private transient int firstReaderHoldCount;
Sync() {
// 初始化 readHolds
readHolds = new ThreadLocalHoldCounter();
// 确保 readHolds 的可见性
setState(getState()); // ensures visibility of readHolds
}
}
读锁
读锁的获取
再回到
ReadLock
部分,获取锁的源代码如下:public void lock() {
sync.acquireShared(1);
} // 与之对应的 AQS 的代码
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
重点在于
tryAcquireShared(arg)
方法,该方法在Sync
中定义:protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState(); /*
exclusiveCount(c) != 0 说明当前有线程持有写锁,在这种情况下就不能直接获取读锁
但是如果持有写锁的线程时当前线程,那么就可以继续获取读锁
*/
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1; int r = sharedCount(c); // 读锁的获取次数 if (!readerShouldBlock() && // 读锁是否需要阻塞
r < MAX_COUNT && // 判断获取锁的次数是否溢出(2^16 - 1)
compareAndSetState(c, c + SHARED_UNIT)) { // 将读锁的获取次数 +1 // 此时已经获取到了读锁 // r == 0 说明线程是第一个获取读锁的,或者前面获取读锁的线程都已经释放了读锁
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++;
} // 返回一个大于 0 的数,表示已经获取到了读锁
return 1;
} return fullTryAcquireShared(current);
}
如果要走到最后一个
return
语句,可能有以下几种情况:readerShouldBlock()
返回true
,这可能有两种情况:在FairSync
中的hasQueuedPredecessors()
方法返回true
,即阻塞队列中存在其它元素在等待锁;在NoFairSync
中的apparentlyFirstQueuedIsExclusive()
方法返回true
,即判断阻塞队列中head
的后继节点是否是用来获取写锁的,如果是的话,那么让这个锁先来,避免写锁饥饿- 持有写锁的数量超过最大值(2^16 - 1)
CAS
失败,即存在竞争关系,可能是多个线程争夺一个读锁,或者多个线程争夺一个写锁
如果是发生了以上几种情况,那么就需要调用
fullTryAcquireShared
再次尝试fullTryAcquireShared(current)
方法对应的源代码:/*
引入这个方法的目的是为了减少锁竞争
*/
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) { // 永真循环避免由于 CAS 失败直接退出的情况
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()) {
// 处理重入
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
/*
cachedHoldCounter 缓存的不是当前的线程,那么到 ThreadLocal 中获取当前线程的 HolderCounter,
如果线程从来没有初始化过 ThreadLocal 的值,那么 get() 方法将会执行初始化
*/
rh = readHolds.get(); /*
rh.count == 0 说明上一行代码只是单纯地初始化,那么它依旧是需要去排队的
*/
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)) {
/*
如果在这里已经 CAS 成功了,那么久意味着成功获取读锁了,
下面要做的就是设置 firstReader 或 cachedHoldCounter
*/ if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
// 设置 cachedHoldCounter 为当前的线程
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; // 大于 0 表示获取到了锁
}
}
}
读锁的释放
ReadLock
释放锁的代码如下:public void unlock() {
sync.releaseShared(1);
}
这个方法位于
AQS
中,具体的定义如下:// 就是一般的释放共享变量的逻辑,具体的模版方法为 tryReleaseShared,需要子类去具体实现
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared(); // 这里是 AQS 的相关知识,再次不做过多的介绍
return true;
}
return false;
}
Sync
中对于tryReleaseShared
的具体实现如下:protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
if (firstReaderHoldCount == 1)
/*
如果 firstReaderHoldCount == 1,那么将 firstReader 置为 null
这是为了给后续的线程使用
*/
firstReader = null;
else
firstReaderHoldCount--;
} else {
// 判断 cachedHoldCounter 是否缓存的是当前的线程,如果不是的话,那么久需要从 ThreadLocal 中获取
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count; if (count <= 1) {
// 这一步将 ThreadLocal 移除掉,这是为了避免内存泄露,因此当前线程已经不再持有读锁了
readHolds.remove();
if (count <= 0)
// unlock() 次数太多了
throw unmatchedUnlockException();
}
--rh.count;
} for (;;) {
int c = getState();
// nextc 是 state 高 16 位 -1 后的值
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
/*
如果 nextc == 0,那就是 state 全部 32 位都为 0,即读锁和写锁都已经全部被释放了
此时在这里返回 true 的话,其实是帮助唤醒后继节点中获取锁的线程
*/
return nextc == 0;
}
}
写锁
写锁的获取
写锁是独占锁,因此如果已经有线程获取到了读锁,那么写锁需要进入到阻塞队列中等待
写锁加锁的源代码:
public void lock() {
sync.acquire(1); // 标准的 AQS 写法
}
重点在于
Sync
类对于tryAcquire
的实现,具体的源代码如下:protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
/*
c != 0 && w == 0: 写锁可用,但是有线程持有读锁(也可能是自己持有)
c != 0 && w != 0 && current != getExclusiveOwnerThread(): 其它线程持有写锁
也就是说,只要有读锁或者写锁被占用,这次就不能获取到写锁
*/
if (w == 0 || current != getExclusiveOwnerThread())
return false; if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded"); // 这里不需要 CAS,因为能够走到这的只可能是写锁重入
setState(c + acquires);
return true;
} // 如果写锁获取不需要阻塞,那么则执行 CAS,成功则代表获取到了写锁
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
写锁的释放
写锁释放的源代码如下:
public void unlock() {
sync.release(1); // 标准的 AQS 的使用
}
与之密切相关的就是
Sync
对于tryRelease
方法的具体实现,具体的实现代码如下所示:// 简单来讲就是将 state 中关于写锁的持有的数量 -1
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException(); int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0; if (free)
setExclusiveOwnerThread(null);
setState(nextc); return free;
}
锁降级
持有写锁的线程,去获取读锁的这个过程被称为锁降级,这样的话,一个线程可能既持有写锁,也持有读锁。但是,是不存在锁升级这个情况的,因为如果一个持有读锁的线程,再去尝试获取写锁,这种情况下就有可能会发生死锁
参考:
[1] https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html
[2] https://javadoop.com/post/reentrant-read-write-lock
Java 并发编程(五)读写锁的更多相关文章
- Java并发编程之读写锁
读写锁维护了一对相关的锁,一个用于只读操作,一个用于写入操作.只要没有writer,读取锁可以由多个reader线程同时保持.写入锁是独占的. 可重入读写锁 ReentrantReadWriteLoc ...
- 【Java并发编程五】信号量
一.概述 技术信号量用来控制能够同时访问某特定资源的活动的数量,或者同时执行某一给定操作的数据.计数信号量可以用来实现资源池或者给一个容器限定边界. 信号量维护了一个许可集,许可的初始量通过构造函数传 ...
- Java并发:ReadWriteLock 读写锁
读写锁在同一时刻可以允许多个线程访问,但是在写线程访问,所有的读线程和其他写线程均被阻塞. 读写锁不像 ReentrantLock 那些排它锁只允许在同一时刻只允许一个线程进行访问,读写锁可以允许多个 ...
- Java多线程编程之读写锁【ReentrantReadWriteLock】
有时候我们需要有这样的需求: 对于同一个文件进行读和写操作,普通的锁是互斥的,这样读的时候会加锁,只能单线程的读,我们希望多线程的进行读操作,并且读的时候不能进行写操作,写的时候不能进行 ...
- java并发锁ReentrantReadWriteLock读写锁源码分析
1.ReentrantReadWriterLock 基础 所谓读写锁,是对访问资源共享锁和排斥锁,一般的重入性语义为如果对资源加了写锁,其他线程无法再获得写锁与读锁,但是持有写锁的线程,可以对资源加读 ...
- Java并发编程 (五) 线程安全性
个人博客网:https://wushaopei.github.io/ (你想要这里多有) 一.安全发布对象-发布与逸出 1.发布与逸出定义 发布对象 : 使一个对象能够被当前范围之外的代码所使用 ...
- java并发编程-StampedLock高性能读写锁
目录 一.读写锁 二.悲观读锁 三.乐观读 欢迎关注我的博客,更多精品知识合集 一.读写锁 在我的<java并发编程>上一篇文章中为大家介绍了<ReentrantLock读写锁> ...
- [Java并发编程(五)] Java volatile 的实现原理
[Java并发编程(五)] Java volatile 的实现原理 简介 在多线程并发编程中 synchronized 和 volatile 都扮演着重要的角色,volatile 是轻量级的 sync ...
- java并发编程笔记(五)——线程安全策略
java并发编程笔记(五)--线程安全策略 不可变得对象 不可变对象需要满足的条件 对象创建以后其状态就不能修改 对象所有的域都是final类型 对象是正确创建的(在对象创建期间,this引用没有逸出 ...
- 读《Java并发编程的艺术》(一)
离开博客园很久了,自从找到工作,到现在基本没有再写过博客了.在大学培养起来的写博客的习惯在慢慢的消失殆尽,感觉汗颜.所以现在要开始重新培养起这个习惯,定期写博客不仅是对自己学习知识的一种沉淀,更是在督 ...
随机推荐
- Shell 文件或目录操作符(-e、-d、-f、-r、-w、-x)
操作符 操作符 含义-e 判断对象是否存在(Exist),若存在则结果为真-d 判断对象是否为目录(Directory),是则为真-f 判断对象是否为一般文件(File),是则为真-r 判断对象是否有 ...
- IDEA工具第一篇:细节使用-习惯设置
安装好Idea后,直接上手clone代码进入编码时代,有没有那么一刻你会觉用起来没有那么顺手流畅呢? 下面是关于 [Windows] 下安装idea的一些习惯设置[ Mac大致一样 ] 一.修改系统文 ...
- Python-文件读取过程中每一行后面带一行空行。贼简单!!!!
关键点在于,将open()函数中,参数为w的一行,格式如下: csvfile = open(data_path + '-21w.csv', 'w') 加上一个参数为newline=' ' 格式如下: ...
- C/C++中的ACM题目输入处理——简单易上手
这里就不按其他文章的以各种情况为分类方法,而是以方法本身为分类办法.因为有一些方法是不同情况通用的,比如已知数量数字的输入和未知数量数字的输入,其实可以用同一种办法. 输入 C/C++ :scanf正 ...
- AI图形算法之一:液位计识别
AI人工智能的主要应用之一就是图形化处理和识别,之前写了两篇,分别是: AI图形算法的应用之一:通过图片模板对比发现油田漏油 AI图形算法的应用之一:仪表识别 经过几个晚上的辛苦,液位计识别也测试成功 ...
- mysql之简单的多表查询
最简单的多表查询需要用到连操作符(join) 1.笛卡儿积 形式为table1 join table2.如: select e.fname,e.lname,d.name from employee e ...
- LCT(link cut tree) 详细图解与应用
樱雪喵用时 3days 做了 ybtoj 的 3 道例题,真是太有效率了!!1 写死自己系列. 为了避免自己没学明白就瞎写东西误人子弟,这篇 Blog 拖到了现在. 图片基本沿用 OIwiki,原文跳 ...
- 二进制枚举&爆搜DFS
给定一个如下图所示的全圆量角器. 初始时,量角器上的指针指向刻度 0. 现在,请你对指针进行 n 次拨动操作,每次操作给定一个拨动角度 ai,由你将指针拨动 ai 度,每次的拨动方向(顺时针或逆时针) ...
- 3.1 IDA Pro编写IDC脚本入门
IDA Pro内置的IDC脚本语言是一种灵活的.C语言风格的脚本语言,旨在帮助逆向工程师更轻松地进行反汇编和静态分析.IDC脚本语言支持变量.表达式.循环.分支.函数等C语言中的常见语法结构,并且还提 ...
- 鸿蒙开发学习(一)之ArkTS
目录 TypeScript语法 基础 module ArkTS 基本UI描述 基本概念 状态管理 页面级变量的状态管理 @State @Prop @Link 应用级变量的状态管理 开发入门 应用模型 ...