JAVA并发(3)-ReentrantReadWriteLock的探索
1. 介绍
本文我们继续探究使用AQS的子类ReentrantReadWriteLock(读写锁)。老规矩,先贴一下类图
ReentrantReadWriteLock这个类包含读锁和写锁,这两种锁都存在是否公平的概念,这个后面会细讲。
此类跟ReentrantLock类似,有以下几种性质:
- 可选的公平性政策
- 重入,读锁和写锁同一个线程可以重复获取。写锁可以获取读锁,反之不能
- 锁的降级,重入还可以通过获取写锁,然后获取到读锁,通过释放写锁的方式,从而写锁降级为读锁。 然而,从读锁升级到写锁是不可能的。
- 获取读写锁期间,支持不可中断
2. 源码剖析
先讲几个必要的知识点,然后我们再对写锁的获取与释放,读锁的获取与释放进行讲解,中间穿插着讲公平与非公平的实现。
知识点一:
内部类Sync中,将AQS中的state(private volatile int state;长度是32位)逻辑分成了两份,高16位代表读锁持有的count,低16位代表写锁持有的count
Sync
...
/*
* Read vs write count extraction constants and functions.
* Lock state is logically divided into two unsigned shorts:
* The lower one representing the exclusive (writer) lock hold count,
* and the upper the shared (reader) hold count.
*/
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
...
This lock supports a maximum of 65535 recursive write locks and 65535 read locks. Attempts to exceed these limits result in Error throws from locking methods.(读锁与写锁都最大支持65535个)
知识点二:
HoldCounter的作用,一个计数器记录每个线程持有的读锁count。使用ThreadLocal维护。缓存在cachedHoldCounter
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
使用ThreadLocal维护
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
维护最后一个使用HoldCounter的线程。简言之就是,假如A线程持有读锁,A线程重入获取读锁,在它之后没有其他线程获取读锁,那么当获取HoldCounter时,可以直接将cachedHoldCounter赋值给该线程,就不用从ThreadLocal中去查询了(ThreadLocal内部维持一个 Map ,想获取当前线程的值就需要去遍历查询),这样做可以节约时间。
private transient HoldCounter cachedHoldCounter;
当前线程持有的重入读锁count,当某个线程持有的count降至0,将被删除。
private transient ThreadLocalHoldCounter readHolds;
初始化在构造函数或readObject中
Sync() {
readHolds = new ThreadLocalHoldCounter();
setState(getState()); // ensures visibility of readHolds
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
readHolds = new ThreadLocalHoldCounter();
setState(0); // reset to unlocked state
}
知识点三:
是否互斥 :
读操作 | 写操作 | |
---|---|---|
读操作 | 否 | 是 |
写操作 | 是 | 是 |
只有读读不互斥,其余都互斥
获取到了写锁,也有资格获取读锁,反之不行.
知识点四
ReentranReadWriteLock,实现了ReadWriteLock接口。
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();
}
ReadWriteLock维护着写锁和读锁。写锁是排他的,而读锁可以同时由多个线程持有。与互斥锁相比,读写锁的粒度更细
有了上面的知识,等会理解下面的源码就更容易了,故事从下面几个变量开始~
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock readLock = rwl.readLock();
private final Lock writeLock = rwl.writeLock();
ReentrantReadWriteLock的构造器,默认是非公平模式
/**
* Creates a new {@code ReentrantReadWriteLock} with
* default (nonfair) ordering properties.
*/
public ReentrantReadWriteLock() {
this(false);
}
/**
* Creates a new {@code ReentrantReadWriteLock} with
* the given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
2.1 写锁
写锁,排他锁;一个线程获取了写锁,其他线程只能等待
2.1.1 写锁的获取
writeLock.lock();
java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock#lock
public void lock() {
sync.acquire(1);
}
又来到了AQS的acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
我们看ReentrantReadWriteLock是如何实现tryAcquire的
protected final boolean tryAcquire(int acquires) {
/*
* 对下面的条件做一个总述:
* 1. 当读锁或者写锁的count不为零时,同时拥有者不是当前线程,返回false
* 2. 当count达到饱和(超过最大的限制65535),返回false
* 3. 如果上面的情况都不是,该线程有资格去获取锁,如果它是可重入获取锁或队列策略允许它。那么就更新state并且设置锁的拥有者
*/
// 结合总述看下面的代码,很容易就看懂了
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
// 存在读锁或写锁
if (c != 0) {
// 情况分析: 1. w = 0, 表示已经获取了读锁(不管是自己还是其他线程),直接返回false
// 2. 写锁不为零,且当前锁的拥有者不是当前线程,返回false
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
// 不存在读锁或写锁被占用的情况
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
上面代码中的writerShouldBlock方法就是tryAcquire控制公平与否的关键,我们分别看看公平与非公平是如何实现的
2.1.1.1 写锁的获取(非公平)
默认情况下是非公平的
static final class NonfairSync extends Sync {
...
// 直接返回false
final boolean writerShouldBlock() {
return false;
}
...
2.1.1.2 写锁的获取(公平)
static final class FairSync extends Sync {
...
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
...
判断是否该线程前面还有其他线程的结点,上一节有讲到过。
这里还贴一下,整个acquire的流程图
2.1.2 写锁的释放
下面的这段代码,记得一定放在finally中
writeLock.unlock();
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
又看到了熟悉的面孔,但我们主要看的还是tryRelease,
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;
}
很简单,就贴一下release的流程图
2.2 读锁
读锁与读锁并不互斥,可以存在多个持有读锁的线程
2.2.1 读锁的获取
readLock.lock();
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
之前的文章还没有讲解过tryAcquireShared在子类如何实现的。看看如何实现的
protected final int tryAcquireShared(int unused) {
/*
* 对下面的条件做一个总述:
* 1. 如果写锁被其他线程持有,失败
* 2. 否则,如果此线程有资格去获取写锁,首先查询是否需要阻塞(readerShouldBlock).如果不需要,就通过CAS更新state。
*
* 3. 如果上面两步都失败了,可能是当前线程没有资格(需要被阻塞),或CAS失败,或者count数量饱和了,然后执行fullTryAcquireShared进行重试
*/
Thread current = Thread.currentThread();
int c = getState();
// 这里可以看出,若该线程持有写锁,同样也可以去获取读锁
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
// readerShouldBlock表示此线程是否被阻塞(后面细谈)
// 多个线程执行compareAndSetState(c, c + SHARED_UNIT), 只有一个线程可以执行成功,其余线程去执行fullTryAcquireShared
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
// firstReader是第一个获取到读锁的线程
// firstReaderHoldCount是firstReader持有的count
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 重入
firstReaderHoldCount++;
} else {
// sync queue还存在其他线程持有读锁,且该线程不是第一个持有读锁的线程
// 结合上面的知识点二,理解下面的逻辑
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;
}
// 到达这里的线程,1. 没有资格获取(需要阻塞) 2. CAS失败 3. 读锁count饱和
return fullTryAcquireShared(current);
}
Full version of acquire for reads, that handles CAS misses and reentrant reads not dealt with in tryAcquireShared.
final int fullTryAcquireShared(Thread current) {
// 主要逻辑跟上面的tryAcquireShared类似。tryAcquireShared的逻辑只有一个线程会成功CAS,
// 其余的线程都进入fullTryAcquireShared,进行重试(代码不那么复杂)
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 reentrantly
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
// 此时, 如果该线程前面已经有线程获取了读锁,且当前线程持有的读锁count为0,从readHolds除去。
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
// 这里表示该线程还是一个new reader, 还没有持有读锁
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 这里只有一个线程会成功,若CAS失败,一直重试直到成功,或者读锁count饱和,或者需要被阻塞为止
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;
}
}
}
我们上面看到了readerShouldBlock,这个方法是控制读锁获取公平与否,下面我们分别看看在非公平与公平模式下的实现
2.2.1.1 读锁的获取(非公平)
static final class NonfairSync extends Sync {
...
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
return apparentlyFirstQueuedIsExclusive();
}
...
}
判断sync queue的head的后继结点是否是写锁(独占模式)
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
上面的方法是,获取读锁时,避免导致写锁饥饿(indefinite writer starvation)的一个措施,下面我们对它进行详细的解释
结合上面的图片,我们设想有一个这样的情况,写锁没有被获取,线程A获取到了读锁,此时另一个线程X想要获取写锁,但是写锁与读锁互斥,所以此时将线程X代表的node添加到sync queue中,等待读锁被释放,才有资格去获取写锁。
上面的情况 + 不存在判断sync queue的head的后继结点是否是写锁(apparentlyFirstQueuedIsExclusive)的方法时,我们看看会出什么问题
时刻一: 线程B、线程C,线程D是新建线程想要去获取读锁(new reader),此时因为不存在写锁被获取,所以线程B、线程C,线程D都会在fullTryAcquireShared中不断重试,最终都获得读锁
时刻二: 线程A释放,会执行unparkSuccessor,此时线程X被唤醒,但是执行到tryAcquire,又检测到读锁被持有(不管是自己还是是其他线程),线程X又被阻塞。线程B释放,还是会出现这种情况,只有等到最后一个读锁被释放,线程X才能获取到写锁。但是想想如果后面一连串的读锁,线程X不是早就被'饿死了'
apparentlyFirstQueuedIsExclusive,就可以防止这种'写锁饥饿'的情况发生。线程B、线程C,线程D只有被阻塞,等待线程X获取到写锁,才有机会获取读锁。
2.2.1.2 读锁的获取(公平)
/**
* Fair version of Sync
*/
static final class FairSync extends Sync {
...
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
...
}
这里关于读锁的获取(公平与非公平)分析完了,贴一张整个acquireShared的流程图
2.2.2 读锁的释放
readLock.unlock();
public void unlock() {
sync.releaseShared(1);
}
AQS
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
// 唤醒head后驱结点
doReleaseShared();
return true;
}
return false;
}
// unused = 1
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
// 获取读锁时,会执行 compareAndSetState(c, c + SHARED_UNIT),这里就是减SHARED_UNIT
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// 释放读锁,对需要读锁的线程没有影响(读读不互斥)
// 但是,这里如果返回true,可以唤醒被阻塞的想要持有写锁的线程
return nextc == 0;
}
}
3. 总结
- ReentrantReadWriteLock中的防止'写锁饥饿'的操作,值得一看
- 将AQS中的state(private volatile int state;),逻辑分为高16位(代表读锁的state),低16位(代表写锁的state),是一个值得学习的办法
- 使用ThreadLocal维护每一个现成的读锁的重入数,使用cachedHoldCounter维护最后一个使用HoldCounter的读锁线程,节省在ThreadLocal中的查询
使用读写锁的情况,应该取决于与修改相比,读取数据的频率,读取和写入操作持续的时间。
例如:
- 某个集合不经常修改,但是对其元素搜索很频繁,使用读写锁就是最佳选择。(简言之,就是读多写少)
- 现在也是读多写少,但是读操作时间很短,只有一小段代码,而读写锁比互斥锁更加复杂,开销可能大于互斥锁,这种情况使用读写锁可能不合适。此时就要通过性能分析,判断使用读写锁在系统中是否合适。
4. 参考
JAVA并发(3)-ReentrantReadWriteLock的探索的更多相关文章
- Java并发编程-ReentrantReadWriteLock
基于AQS的前世今生,来学习并发工具类ReentrantReadWriteLock.本文将从ReentrantReadWriteLock的产生背景.源码原理解析和应用来学习这个并发工具类. 1. 产生 ...
- Java并发编程,深度探索J.U.C - AQS
java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心. CountdownLatch 用来控制一个线程等待多个线程. 维护了一个计数器 cnt ...
- java并发锁ReentrantReadWriteLock读写锁源码分析
1.ReentrantReadWriterLock 基础 所谓读写锁,是对访问资源共享锁和排斥锁,一般的重入性语义为如果对资源加了写锁,其他线程无法再获得写锁与读锁,但是持有写锁的线程,可以对资源加读 ...
- java 并发(五)---AbstractQueuedSynchronizer(4)
问题 : rwl 的底层实现是什么,应用场景是什么 读写锁 ReentrantReadWriteLock 首先我们来了解一下 ReentrantReadWriteLock 的作用是什么?和 Reent ...
- 【Java并发编程实战】-----“J.U.C”:ReentrantReadWriteLock
ReentrantLock实现了标准的互斥操作,也就是说在某一时刻只有有一个线程持有锁.ReentrantLock采用这种独占的保守锁直接,在一定程度上减低了吞吐量.在这种情况下任何的"读/ ...
- java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock
原文:java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock 锁 锁是用来控制多个线程访问共享资源的方式,java中可以使用synch ...
- Java并发(十):读写锁ReentrantReadWriteLock
先做总结: 1.为什么用读写锁 ReentrantReadWriteLock? 重入锁ReentrantLock是排他锁,在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供读服 ...
- JAVA并发-ReentrantReadWriteLock
简介 读写锁维护着一对锁,一个读锁和一个写锁.通过分离读锁和写锁,使得并发性比一般的排他锁有了较大的提升:在同一时间可以允许多个读线程同时访问,但是在写线程访问时,所有读线程和写线程都会被阻塞. 读写 ...
- Java并发编程总结3——AQS、ReentrantLock、ReentrantReadWriteLock(转)
本文内容主要总结自<Java并发编程的艺术>第5章——Java中的锁. 一.AQS AbstractQueuedSynchronizer(简称AQS),队列同步器,是用来构建锁或者其他同步 ...
随机推荐
- 自动化kolla-ansible部署ubuntu20.04+openstack-victoria之ceph部署-07
自动化kolla-ansible部署ubuntu20.04+openstack-victoria之ceph部署-07 欢迎加QQ群:1026880196 进行交流学习 近期我发现网上有人转载或者复制原 ...
- Go-08-函数与指针
Go语言的函数本身可以作为值进行传递,既支持匿名函数和闭包,又能满足接口. 函数声明 func 函数名 (参数列表)(返回参数列表){ // 函数体 } func funcName(parameter ...
- Spring-@PostConstruct注解
@PostConstruct注解 @PostConstruct注解好多人以为是Spring提供的.其实是Java自己的注解. Java中该注解的说明:@PostConstruct该注解被用来修饰一个非 ...
- EhCache缓存使用教程
文章发表在我的博客上:https://blog.ysboke.cn/archives/124.html 什么是ehcache 纯Java的进程内缓存,直接在JVM虚拟机中缓存,速度非常快.缓存有两级, ...
- 从苏宁电器到卡巴斯基第10篇:我在苏宁电器当营业员 II
之所以是主推,其实是有原因的 据我所知,尽管诺基亚卖的很好,但是他们的厂促的待遇却很一般,估计也就一千多两千的样子,撑死两千多.但是呢,记得当时我们的卖场里面还有联想手机,别看卖得相当次,但是他们的厂 ...
- Windows Pe 第三章 PE头文件(上)
第三章 PE头文件 本章是全书重点,所以要好好理解,概念比较多,但是非常重要. PE头文件记录了PE文件中所有的数据的组织方式,它类似于一本书的目录,通过目录我们可以快速定位到某个具体的章节:通过P ...
- Docker Swarm删除节点
节点上的主机如果想离开的话,可以自己直接执行docker swarm leave 然后你可以发现,原本跑在自己上面的容器被转移到别的容器上了.此时如果在manager节点上docker node ls ...
- postgresql高级应用之合并单元格
postgresql高级应用之合并单元格 转载请注明出处https://www.cnblogs.com/funnyzpc/p/14732172.html 1.写在前面✍ 继上一篇postgresql高 ...
- 十进制转n进制
#include <stdio.h> #include <stdlib.h> #define OK 1 #define ERROR 0 #define TRUE 1 #defi ...
- 【Redis】redis异步消息队列+Spring自定义注解+AOP方式实现系统日志持久化
说明: SSM项目中的每一个请求都需要进行日志记录操作.一般操作做的思路是:使用springAOP思想,对指定的方法进行拦截.拼装日志信息实体,然后持久化到数据库中.可是仔细想一下会发现:每次的客户端 ...