【Java并发】JUC—ReentrantReadWriteLock有坑,小心读锁!
好长一段时间前,某些场景需要JUC的读写锁,但在某个时刻内读写线程都报超时预警(长时间无响应),看起来像是锁竞争过程中出现死锁(我猜)。经过排查项目并没有能造成死锁的可疑之处,因为业务代码并不复杂(仅仅是一个计算过程),经几番折腾,把注意力转移到JDK源码,正文详细说下ReentrantReadWriteLock的隐藏坑点。
过程大致如下:
- 若干个读写线程抢占读写锁
- 读线程手脚快,优先抢占到读锁(其中少数线程任务较重,执行时间较长)
- 写线程随即尝试获取写锁,未成功,进入双列表进行等待
- 随后读线程也进来了,要去拿读锁
问题:优先得到锁的读线程执行时间长达73秒,该时段写线程等待是理所当然的,那读线程也应该能够得到读锁才对,因为是共享锁,是吧?但预警结果并不是如此,超时任务线程中大部分为读。究竟是什么让读线程无法抢占到读锁,而导致响应超时呢?
把场景简化为如下的测试代码:读——写——读 线程依次尝试获取ReadWriteLock,用空转替换执行时间过长。
执行结果:控制台仅打印出Thread[读线程 -- 1,5,main],既是说读线程 -- 2并没有抢占到读锁,跟上诉的表现似乎一样。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
public class ReadWriteLockTest {
public static void main(String[] args) {
ReadWriteLockTest readWriteLockTest = new ReadWriteLockTest();
}
public ReadWriteLockTest() {
try {
init();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
void init() throws InterruptedException {
TestLock testLock = new TestLock();
Thread read1 = new Thread(new ReadThread(testLock), "读线程 -- 1");
read1.start();
Thread.sleep(100);
Thread write = new Thread(new WriteThread(testLock), "写线程 -- 1");
write.start();
Thread.sleep(100);
Thread read2 = new Thread(new ReadThread(testLock), "读线程 -- 2");
read2.start();
}
private class TestLock {
private String string = null;
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock readLock = readWriteLock.readLock();
private Lock writeLock = readWriteLock.writeLock();
public void set(String s) {
writeLock.lock();
try {
// writeLock.tryLock(10, TimeUnit.SECONDS);
string = s;
} finally {
writeLock.unlock();
}
}
public String getString() {
readLock.lock();
System.out.println(Thread.currentThread());
try {
while (true) {
}
} finally {
readLock.unlock();
}
}
}
class WriteThread implements Runnable {
private TestLock testLock;
public WriteThread(TestLock testLock) {
this.testLock = testLock;
}
@Override
public void run() {
testLock.set("射不进去,怎么办?");
}
}
class ReadThread implements Runnable {
private TestLock testLock;
public ReadThread(TestLock testLock) {
this.testLock = testLock;
}
@Override
public void run() {
testLock.getString();
}
}
}
|
我们用jstack查看一下线程,看到读线程2和写线程1确实处于WAITING的状态。
jstack
排查项目后,业务代码并没有问题,转而看下ReentrantReadWriteLock或AQS是否有什么问题被我忽略的。
第一时间关注共享锁,因为独占锁的实现逻辑我确定很清晰了,很快我似乎看到自己想要的方法。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
public static class ReadLock implements Lock, java.io.Serializable {
public void lock() {
//if(tryAcquireShared(arg) < 0) doAcquireShared(arg);
sync.acquireShared(1);
}
}
abstract static class Sync extends AbstractQueuedSynchronizer {
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//计算stata,若独占锁被占,且持有锁非本线程,返回-1等待挂起
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//计算获取共享锁的线程数
int r = sharedCount(c);
//readerShouldBlock检查读线程是否要阻塞
if (!readerShouldBlock() &&
//线程数必须少于65535
r < MAX_COUNT &&
//符合上诉两个条件,CAS(r, r+1)
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);
}
}
|
嗯,没错,方法readerShouldBlock()十分瞩目,几乎不用看上下文就定位到该方法。因为默认非公平锁,所以直接关注NonfairSync。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
static final class NonfairSync extends Sync {
final boolean writerShouldBlock() {
return false;
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
//下面方法在ASQ中
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null && //head非空
(s = h.next) != null && //后续节点非空
!s.isShared() && //后续节点是否为写线程
s.thread != null; //后续节点线程非空
}
|
apparentlyFirstQueuedIsExclusive什么作用,检查持锁线程head后续节点s是否为写锁,若真则返回true。结合tryAcquireShared的逻辑,如果true意味着读线程会被挂起无法共享锁。
这好像就说得通了,当持锁的是读线程时,跟随其后的是一个写线程,那么再后面来的读线程是无法获取读锁的,只有等待写线程执行完后,才能竞争。
这是jdk为了避免写线程过分饥渴,而做出的策略。但有坑点就是,如果某一读线程执行时间过长,甚至陷入死循环,后续线程会无限期挂起,严重程度堪比死锁。为避免这种情况,除了确保读线程不会有问题外,尽量用tryLock,超时我们可以做出响应。
当然也可以自己实现ReentrantReadWriteLock的读写锁竞争策略,但还是算了吧,遇到读远多于写的场景时,写线程饥渴带来的麻烦更大,表示踩过坑,别介。
from: http://huangzehong.me/2018/07/02/20180702%20-%E3%80%90Java%E5%B9%B6%E5%8F%91%E3%80%91JUC%E2%80%94ReentrantReadWriteLock%E6%9C%89%E5%9D%91%EF%BC%8C%E5%B0%8F%E5%BF%83%E8%AF%BB%E9%94%81%EF%BC%81/
【Java并发】JUC—ReentrantReadWriteLock有坑,小心读锁!的更多相关文章
- JAVA并发(3)-ReentrantReadWriteLock的探索
1. 介绍 本文我们继续探究使用AQS的子类ReentrantReadWriteLock(读写锁).老规矩,先贴一下类图 ReentrantReadWriteLock这个类包含读锁和写锁,这两种锁都存 ...
- Java并发编程-ReentrantReadWriteLock
基于AQS的前世今生,来学习并发工具类ReentrantReadWriteLock.本文将从ReentrantReadWriteLock的产生背景.源码原理解析和应用来学习这个并发工具类. 1. 产生 ...
- java并发锁ReentrantReadWriteLock读写锁源码分析
1.ReentrantReadWriterLock 基础 所谓读写锁,是对访问资源共享锁和排斥锁,一般的重入性语义为如果对资源加了写锁,其他线程无法再获得写锁与读锁,但是持有写锁的线程,可以对资源加读 ...
- java 并发(五)---AbstractQueuedSynchronizer(4)
问题 : rwl 的底层实现是什么,应用场景是什么 读写锁 ReentrantReadWriteLock 首先我们来了解一下 ReentrantReadWriteLock 的作用是什么?和 Reent ...
- 【Java并发编程实战】-----“J.U.C”:ReentrantReadWriteLock
ReentrantLock实现了标准的互斥操作,也就是说在某一时刻只有有一个线程持有锁.ReentrantLock采用这种独占的保守锁直接,在一定程度上减低了吞吐量.在这种情况下任何的"读/ ...
- Java - "JUC" ReentrantReadWriteLock
Java多线程系列--“JUC锁”08之 共享锁和ReentrantReadWriteLock ReadWriteLock 和 ReentrantReadWriteLock介绍 ReadWriteLo ...
- java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock
原文:java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock 锁 锁是用来控制多个线程访问共享资源的方式,java中可以使用synch ...
- Java并发(十):读写锁ReentrantReadWriteLock
先做总结: 1.为什么用读写锁 ReentrantReadWriteLock? 重入锁ReentrantLock是排他锁,在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供读服 ...
- JAVA并发-ReentrantReadWriteLock
简介 读写锁维护着一对锁,一个读锁和一个写锁.通过分离读锁和写锁,使得并发性比一般的排他锁有了较大的提升:在同一时间可以允许多个读线程同时访问,但是在写线程访问时,所有读线程和写线程都会被阻塞. 读写 ...
随机推荐
- 【LOJ】#2081. 「JSOI2016」反质数序列
题解 我居然都没反应过来二分图内选集合两两不能有边是最大独立集了 我退役吧 显然连边只能在奇数和偶数之间,然后二分图求最大独立集是节点数-最大匹配数 啊当然还有对于1的话只能留一个1 代码 #incl ...
- H5中canvas标签制作在线画板
1.介绍 左键点击下移动开始画图.放开鼠标不在画图. 2.重要使用理论 query的使用: 鼠标的按下mousedown 鼠标的移动mousemove 鼠标的放开 mouseup 3.程序 <! ...
- jqplot导入包小结
对于jqplot画图的导入包,总结起来就是两种,一种是每个jsp文件都是导入一样的js或css包!这些包可以另新建一个文件存放,有如下这些包! <link rel="styleshee ...
- Centos 7.4下 部署openstack Queens 计算节点qemu高版本问题
sed -i 's/$contentdir/centos/g' /etc/yum.repos.d/CentOS-QEMU-EV.repo 这样既可正常安装compute服务
- 【FFT&NTT 总结】
$FFT$总结 (因为还不会啊,,都没做过什么题,所以一边学一边打咯.. 1.主要是用来加速卷积形式的求和吧? $F*G(n)=F[i] × G[n-i]$ 平时是$O(n^2)$的,FFT可以$O( ...
- [TC13761]Mutalisk
[TC13761]Mutalisk 题目大意: 有\(n(n\le20)\)个坏人,第\(i\)个坏人的血量为\(A_i(A_i\le60)\).你可以每次攻击\(3\)个坏人,并分别造成\(9\)点 ...
- Android笔记(二):savedIndstanceState 和 Bundle
savedIndstanceState savedIndstanceState 位于 Activity onCreate(Bundle savedInstanceState)方法的参数中.对这个参数的 ...
- 关于java中的锁(转)
对于锁一直处于比较模糊的状态,最近一天晚上偶然想看看,就翻了几本书,然后弄明白了一些概念,有一些仍然没明白,例如AQS,先把搞明白的记录一下吧. 什么是线程安全? 当多个线程访问一个对象时,如果不用考 ...
- AT91 USB Composite Driver Implementation
AT91 USB Composite Driver Implementation 1. Introduction The USB Composite Device is a general way t ...
- 微信公众号 JSSDK 提示:invalid signature
要命的invalid signature.其实腾讯的文档已经写了,只能怪我自己理解能力太差,掉了好几次坑. 签名要用到的jsapi_ticket需要保存的,2小时有效期.如果在2小时内出现问题需要删除 ...