好长一段时间前,某些场景需要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有坑,小心读锁!的更多相关文章

  1. JAVA并发(3)-ReentrantReadWriteLock的探索

    1. 介绍 本文我们继续探究使用AQS的子类ReentrantReadWriteLock(读写锁).老规矩,先贴一下类图 ReentrantReadWriteLock这个类包含读锁和写锁,这两种锁都存 ...

  2. Java并发编程-ReentrantReadWriteLock

    基于AQS的前世今生,来学习并发工具类ReentrantReadWriteLock.本文将从ReentrantReadWriteLock的产生背景.源码原理解析和应用来学习这个并发工具类. 1. 产生 ...

  3. java并发锁ReentrantReadWriteLock读写锁源码分析

    1.ReentrantReadWriterLock 基础 所谓读写锁,是对访问资源共享锁和排斥锁,一般的重入性语义为如果对资源加了写锁,其他线程无法再获得写锁与读锁,但是持有写锁的线程,可以对资源加读 ...

  4. java 并发(五)---AbstractQueuedSynchronizer(4)

    问题 : rwl 的底层实现是什么,应用场景是什么 读写锁 ReentrantReadWriteLock 首先我们来了解一下 ReentrantReadWriteLock 的作用是什么?和 Reent ...

  5. 【Java并发编程实战】-----“J.U.C”:ReentrantReadWriteLock

    ReentrantLock实现了标准的互斥操作,也就是说在某一时刻只有有一个线程持有锁.ReentrantLock采用这种独占的保守锁直接,在一定程度上减低了吞吐量.在这种情况下任何的"读/ ...

  6. Java - "JUC" ReentrantReadWriteLock

    Java多线程系列--“JUC锁”08之 共享锁和ReentrantReadWriteLock ReadWriteLock 和 ReentrantReadWriteLock介绍 ReadWriteLo ...

  7. java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock

    原文:java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock 锁 锁是用来控制多个线程访问共享资源的方式,java中可以使用synch ...

  8. Java并发(十):读写锁ReentrantReadWriteLock

    先做总结: 1.为什么用读写锁 ReentrantReadWriteLock? 重入锁ReentrantLock是排他锁,在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供读服 ...

  9. JAVA并发-ReentrantReadWriteLock

    简介 读写锁维护着一对锁,一个读锁和一个写锁.通过分离读锁和写锁,使得并发性比一般的排他锁有了较大的提升:在同一时间可以允许多个读线程同时访问,但是在写线程访问时,所有读线程和写线程都会被阻塞. 读写 ...

随机推荐

  1. jQuery库冲突

    jQuery库冲突解决办法   一次面试中面试官问到jQuery和别的库冲突怎么解决?虽然以前看过,但是我已经不记得了. 我的思路就是如果让我来设计,那我就用一个默认值$,不传参数,那就用$,最后就挂 ...

  2. zabbix agent配置文件记录

    一.无论主动还是被动模式都要配置server和linstenPort 二.若要设置主动模式那么要添加ServerActive,若不添加则默认为被动模式

  3. 004.KVM日常管理1

    一 常用命令 1.1 查看虚机列表及状态 [root@kvm-host ~]# virsh list --all 1.2 连接虚机 [root@kvm-host ~]# virsh console v ...

  4. SpringBoot详细研究-05微服务与拾遗

    这将是SpringBoot学习系列的最后一篇文章,主要涉及Spring Cloud和JHispter两个知识点,都具有一定综合性,需要多加实践加强理解. Spring Cloud 微服务Microse ...

  5. Redis高可用之集群配置(六)

    0.Redis目录结构 1)Redis介绍及部署在CentOS7上(一) 2)Redis指令与数据结构(二) 3)Redis客户端连接以及持久化数据(三) 4)Redis高可用之主从复制实践(四) 5 ...

  6. 任何一个大于1的自然数n,总可以拆分成若干个小于n的自然数之和。

    任何一个大于1的自然数n,总可以拆分成若干个小于n的自然数之和. 当n=7共14种拆分方法: 7=1+1+1+1+1+1+1 7=1+1+1+1+1+2 7=1+1+1+1+3 7=1+1+1+2+2 ...

  7. GEEK-2018之隐藏在混乱之中的绝密情报 writeup

    题目如上 打开题目后发现,提示robots 随后提示又需要改一改名字 修改文件名为humans.txt之后发现 有个备份文件,直接访问www.zip就可以下载了 下载打开后如下: 在其中看到了unse ...

  8. java基础记录(一):开发环境的配置

    一.JDK的安装与环境变量配置 1.jdk下载与安装. jdk1.8.0_192下载地址 下载完成后,双击运行安装文件.可以选择你要安装的位置或者直接下一步,等待安装完成,最后关闭. 2.配置环境变量 ...

  9. 15、Redis的集群

     写在前面的话:读书破万卷,编码如有神 -------------------------------------------------------------------------------- ...

  10. Codeforces Round #404 (Div. 2) A - Anton and Polyhedrons 水题

    A - Anton and Polyhedrons 题目连接: http://codeforces.com/contest/785/problem/A Description Anton's favo ...