1. 问题

最近有同事问了我一个问题,在Java编程中,当有一条线程要获取ReentrantReadWriteLock的读锁,此时已经有其他线程获得了读锁,AQS队列里也有线程在等待写锁。由于读锁是共享锁,当前线程是马上获得读锁,还是排队?如果是马上获得读锁,那岂不是阻塞的等待写锁的线程有可能一直(或长时间)拿不到写锁(写锁饥饿)?

带着这个问题,我打开读写锁的源码,来看一下JDK是怎么实现的。(注:读写锁指ReentrantReadWriteLock, 以下说到的读锁和写锁,都是指属于同一个读写锁的情况。读锁和共享锁,写锁和独占锁,在这里是同样的意思。如无特殊说明,提到的模式都是默认的非公平模式)

2. JUC万物皆有AQS

2.1 读锁的实现。

先来看看读锁的实现。持有一个AQS,所以说,JUC万物皆有AQS(大雾)。

顺便提一下写锁,写锁也是类似的实现,而且传入的是同一个读写锁,那么读锁和写锁,都拥有同一个AQS,这样才能实现互相阻塞。

读锁是共享模式。

2.2 tryAcquireShared(int arg)的实现。

熟悉AQS的同学就知道,共享锁的实现,AQS已经写好了流程。但留下了一个钩子,tryAcquireShared(int arg) 供各种场景实现。

那么我们就来看看,读写锁里面,共享锁(读锁)是怎么实现的。

step1. 红框一,如果当前已经有线程持有了独占锁(即写锁),且不是当前线程持有,那么无法重入,直接返回-1,获取共享锁失败。

step2. 如果step1的情况被排除,那么进行readerShouldBlock()的判断。在读写锁中,AQS有两种实现,公平和非公平模式,默认是非公平模式。

也就是说,上面所说的sync变量的实际类型,可以是公平模式,也可以是非公平模式。

因此,readerShouldBlock()也有公平和非公平两种不同的实现。

公平模式下,只要前面有阻塞排队的节点,就返回true,表示不能抢占。

非公平模式下,看看第一个等待的阻塞节点是不是独占式的,如果是,返回true,有可能不可以抢在人家前面(为什么是有可能?要考虑可重入的场景,下面分析)。这是为了避免写锁饥饿。

所以,如果readerShouldBlock()返回false,并且读锁获取的总次数不溢出,且CAS成功,说明获取共享锁成功,下面进入if块,设置一些变量,并将当前线程持有的该读锁的次数递增加1,返回成功标志。

看到这里,也许你会有疑惑,仅仅是因为CAS失败,就获取共享锁失败了吗?而且,ReentrantReadWriteLock是一个可重入锁,这里也没看到有重入的地方啊。

别急,如果step2失败,会进入step3,到第三个红框,进入fullTryAcquireShared(Thread current)方法。

2.3  final int fullTryAcquireShared(Thread current)

这个方法比较长,里面用了for(;;) 自旋CAS,为什么呢?因为CAS还是可能会失败啊……失败就得继续再尝试一把。

我就贴出for(;;) 里的代码,分为两段,第一段判断是否可以尝试获取锁(与上面类似,加了重入的判断),第二段CAS和成功后的一些操作。

先看第一段,判断是否可以尝试获取锁。

step1. 如果有线程持有独占锁,并且不是当前线程,返回失败标志-1。如果是当前线程,由于可重入的语义,通过了判断,直接跑到第二段代码了。说明在持有独占锁的情况下可以获取共享锁(锁降级)。

step2. 如果当前没有线程持有独占锁,那么再来看看熟悉的readerShouldBlock()。通过上面的分析我们知道,在公平模式下有节点在阻塞就得排队,在非公平模式下有可能不可以抢在人家前面。为什么是有可能?因为要考虑可重入的场景。

如果firstReader是当前线程,或者当前线程的cachedHoldCounter变量的count不为0(表示当前线程已经持有了该共享锁),均说明当前线程已经持有共享锁,此次获取共享锁是重入,这也是允许的,可以通过判断。

如果可以顺利通过上面两步判断,说明获取共享锁成功,下面开始熟悉的CAS。

失败了咋办?别忘记是自旋啊,外层是for(;;),那就再来一发~~。当然还得再来一遍第一段的判断。

3. 结论

经过上面的分析,可以来回答我的同事的问题了。

在Java编程中,当有一条线程要获取ReentrantReadWriteLock的读锁,此时已经有其他线程获得了读锁,AQS队列里也有线程在等待写锁。由于读锁是共享锁,当前线程是马上获得读锁,还是排队?如果是马上获得读锁,那岂不是阻塞的等待写锁的线程有可能一直(或长时间)拿不到写锁(写锁饥饿)?

1.如果已经有线程持有独占锁

1.1 该线程不是当前线程,不用想了,乖乖排队;

1.2 该线程就是当前线程,重入,CAS获取共享锁;

2.如果没有线程持有独占锁,检查当前线程是否需要block(readerShouldBlock方法)。

block的判断,有两种模式,公平和非公平(默认模式)。如果不需要block, 必须满足:公平模式下,没有节点在AQS等待;非公平模式下,AQS第一个等待的节点不是独占式的;

2.1 不需要block,可以CAS获取共享锁;

2.2 需要block;

2.2.1 当前线程已经持有了共享锁,重入,还是可以CAS获取共享锁;

2.2.2 当前线程前没有已经持有共享锁,则获取失败,只能排队。

上面是根据代码逻辑整理的,可以换为更简洁的语言。

如果当前线程已经持有独占锁或共享锁(重入)或不需要block,则CAS获取共享锁;否则,排队。

readerShouldBlock()判断第一个节点是获取共享锁或独占锁,在不考虑重入的情况下,是什么意思呢?

1. 第一个节点是等待独占锁的场景,说明下一个就是它了,不能抢它的,抢不到;

2. 第一个节点是等待共享锁的场景,说明第一个节点,

2.1 在等待持有独占锁的线程释放独占锁,这种必然是抢不到的。

2.2 持有共享锁的线程还在唤醒后续节点的过程中,允许你去抢一下。当然,不意味着一定可以抢成功。

如果是2.2持有共享锁的线程在唤醒后续节点过程中,理论上是可能获取得到的。这种情况概率较小,我没重现过。

回到这个问题。当前线程并没有获取到写锁或读锁,不能重入;AQS中,第一个等待的大概率是想要获取独占锁的节点,必须block,所以当前线程只能排队,并不会出现阻塞的想获取写锁的节点一直拿不到写锁的情况;如果刚好没有完全唤醒,那么可能是可以抢占的。但也不会一直阻塞,因为唤醒节点获取读锁的过程是很快的。

总之,获取读锁的机制,记住这个结论就行。

如果当前线程已经持有独占锁或共享锁(重入)或不需要block,则CAS获取共享锁;否则,排队。

4. 举个栗子

第一个节点是独占锁的场景,不能抢占

 package com.khlin.my.test;

 import java.util.concurrent.locks.ReentrantReadWriteLock;

 public class RRWLockTest {

     public static void main(String[] args) throws InterruptedException {
final ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock(); Thread reader1 = new Thread(new Runnable() {
public void run() {
try {
LOCK.readLock().lock();
System.out.println("reader1 locked.");
Thread.sleep(3000L);
System.out.println("reader1 finished.");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
LOCK.readLock().unlock();
}
}
}); Thread reader2 = new Thread(new Runnable() {
public void run() {
try {
LOCK.readLock().lock();
System.out.println("reader2 locked.");
System.out.println("reader2 finished.");
} finally {
LOCK.readLock().unlock();
}
}
}); Thread writer = new Thread(new Runnable() {
public void run() {
try{
LOCK.writeLock().lock();
System.out.println("writer locked.");
System.out.println("writer finished.");
}finally {
LOCK.writeLock().unlock();
}
}
});
reader1.start();
Thread.sleep(1000L);
writer.start();
Thread.sleep(1000L);
reader2.start();
}
}

reader1获取了读锁,正在执行,随后writer来获取写锁,失败,入队等待。reader2由于writer正在等待(通过readerShouldBlock判断),无法获取读锁,入队,等待。输出如下:

简单分析线程获取ReentrantReadWriteLock 读锁的规则的更多相关文章

  1. [源码分析]读写锁ReentrantReadWriteLock

    一.简介 读写锁. 读锁之间是共享的. 写锁是独占的. 首先声明一点: 我在分析源码的时候, 把jdk源码复制出来进行中文的注释, 有时还进行编译调试什么的, 为了避免和jdk原生的类混淆, 我在类前 ...

  2. 简单分析ThreadPoolExecutor回收工作线程的原理

    最近阅读了JDK线程池ThreadPoolExecutor的源码,对线程池执行任务的流程有了大体了解,实际上这个流程也十分通俗易懂,就不再赘述了,别人写的比我好多了. 不过,我倒是对线程池是如何回收工 ...

  3. ffplay.c函数结构简单分析(画图)

    最近重温了一下FFplay的源代码.FFplay是FFmpeg项目提供的播放器示例.尽管FFplay只是一个简单的播放器示例,它的源代码的量也是不少的.之前看代码,主要是集中于某一个"点&q ...

  4. ffplay.c函数结构简单分析(绘图)

    近期重温了一下FFplay的源码. FFplay是FFmpeg项目提供的播放器演示样例.虽然FFplay不过一个简单的播放器演示样例,它的源码的量也是不少的. 之前看代码,主要是集中于某一个" ...

  5. SpringCloud配置刷新机制的简单分析[nacos为例子]

    SpringCloud Nacos 本文主要分为SpringCloud Nacos的设计思路 简单分析一下触发刷新事件后发生的过程以及一些踩坑经验 org.springframework.cloud. ...

  6. AbstractQueuedSynchronizer的简单分析

    说明:本作者是文章的原创作者,转载请注明出处:本文地址:http://www.cnblogs.com/qm-article/p/7955781.html 一.AbstractQueuedSynchro ...

  7. x264源代码简单分析:宏块分析(Analysis)部分-帧间宏块(Inter)

    ===================================================== H.264源代码分析文章列表: [编码 - x264] x264源代码简单分析:概述 x26 ...

  8. x264源代码简单分析:滤波(Filter)部分

    ===================================================== H.264源代码分析文章列表: [编码 - x264] x264源代码简单分析:概述 x26 ...

  9. x264源代码简单分析:编码器主干部分-1

    ===================================================== H.264源代码分析文章列表: [编码 - x264] x264源代码简单分析:概述 x26 ...

随机推荐

  1. BI-学习之 商业智能项目工具安装

    首先咱们先需要下载一个工具,Microsoft SQL Server Data Tools - Business Intelligence for Visual Studio 2012并安装: 我目前 ...

  2. Qt之获取本机网络信息(超详细)

    经常使用命令行来查看一些计算机的配置信息. 1.首先按住键盘上的“开始键+R键”,然后在弹出的对话框中输入“CMD”,回车 另外,还可以依次点击 开始>所有程序>附件>命令提示符 2 ...

  3. 转载几篇文章URL

    读了百伯在线Jobbole的几篇文章,转给需要的朋友.如下: 产品小设计大体验:http://blog.jobbole.com/39593/ 苹果是一家有工程师的设计公司:Google是一家有设计师的 ...

  4. 3015C语言_流程设计

    第五章 流程设计 5.1 C语句概述 C语言的语句用来向计算机系统发出指令,一个实际的源程序通常包含若干语句,这些语句用来完成一定的操作任务. 1.其他类型语句 函数调用语句(由函数调用加一个分号构成 ...

  5. Scala 学习之路(二)—— 基本数据类型和运算符

    一.数据类型 1.1 类型支持 Scala 拥有下表所示的数据类型,其中Byte.Short.Int.Long和Char类型统称为整数类型,整数类型加上Float和Double统称为数值类型.Scal ...

  6. 怎么用Hostwinds搭建Wordpress博客网站(超详细图文教程)

    Hostwinds 成立于 2010 年,在主机托管行业算是一个比较新的品牌,但是,凭借丰富的产品线.卓越的服务器性能.良好的客户支持,以及低廉实惠的价格,他们受到了广大客户的喜爱,并多次获得行业重要 ...

  7. Navicat for MySQ v11-v12都有的,供大家学习提升使用

    Navicat for MySQL破解版是一套专为 MySQL 设计的高性能数据库管理及开发工具,Navicat for MySQL破解版主要功能包括SQL创建工具或编辑器.数据模型工具.数据传输.导 ...

  8. 前端学习【第一篇】: HTML内容

    内容概要: HTML介绍 常用标签介绍 一. HTML介绍 web服务的本质 #!/usr/bin/env python3 # _*_ coding:utf- _*_ import socket sk ...

  9. 高性能微服务网关.NETCore客户端Kong.Net开源发布

    前言 项目地址:https://github.com/lianggx/Kong.Net 你的支持使我们更加强大,请单击 star 让更多的 .NETCore 认识它. 拥抱开源的脚步,我们从来都是一直 ...

  10. Win32小游戏--蜘蛛纸牌

    前一段时间完成了蜘蛛纸牌的仿写,现将过程和思路记录下来 首先,为了符合复用性,在win32的基本框架中,把可变的部分用c++封装起来成为一系列虚函数,这样如果再继续写游戏的话,只需要继承这个类就可以了 ...