ReentrantLock可重入锁——源码详解
开始这篇博客之前,博主默认大家都是看过AQS源码的~什么居然没看过猛戳下方
全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(一)AQS基础
全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(二)资源的获取和释放
全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(三)条件变量
介绍
ReentrantLock
是可重入锁,是JUC提供的一种最常用的锁。“可重入”的意思就是:同一个线程可以无条件地反复获得已经持有的锁
ReentrantLock
有公平锁和非公平锁两种模式,底层使用的正是AbstractQueuedSynchronizer
这个伟大的并发工具
ReentrantLock
的结构如下图所示:
ReentrantLock
实现了Lock
接口,该接口定义了一个锁应该具备的基本功能,即加锁、解锁、创建条件变量等功能。源码如下:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
使用ReentrantLock
,主要使用lock
和unlock
方法,也会用到newCondition
来创建条件变量,实现一些条件同步功能
回到上面的结构图,可以看到,ReentrantLock
的功能主要是借助其内部类Sync
来实现,而Sync类是继承了AbstractQueuedSynchronizer
,并衍生出两个子类FairSync
、NonfairSync
,分别对应公平锁和非公平锁两种模式。实际应用中,一般非公平锁的效率要高于公平锁
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
Sync
ReentrantLock
中的sync
域是是一个Sync
类对象,ReentrantLock
使用Sync
类实现主要的功能,Sync
是ReentrantLock
的内部类:
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
Sync
类继承了AQS,使用AQS的state
作为锁的重入数,其源码如下:
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
// 用于辅助ReentrantLock执行Lock接口的lock方法,所以定义了一个同名lock方法
abstract void lock();
// 执行非公平的tryLock,是非公平锁子类实现的tryAcquire方法的主要逻辑,也是ReentrantLock的tryLock的主要逻辑?
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
// 为了支持使用条件变量,Sync实现了AQS中的isHeldExclusively,并提供了newCondition方法创建条件变量
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
// ReentrantLock的三个同名方法,都委托给了下面这三个方法,用于获取锁的一些信息
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
final boolean isLocked() {
return getState() != 0;
}
/**
* Reconstitutes the instance from a stream (that is, deserializes it).
*/
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
总结一下,Sync
类有以下几个作用:
- 定义抽象
lock
方法:Sync
定义了lock
这一个抽象方法,强迫两个子类去实现,而ReentrantLock
的lock
方法就可以直接委托给Sync
的lock
方法去执行 - 非公平模式的尝试获取锁:
Sync
提供了nonfairTryAcquire
方法,提供非公平尝试获取锁的方法。不仅非公平子类实现tryAcquire
方法需要委托给nonfairTryAcquire
来处理,而且ReentrantLock
中的tryLock
方法也会委托给它来处理 - 尝试释放锁:
Sync
实现了AQS中的tryRelease
方法,因为不管是公平模式还是非公平模式,释放锁的逻辑都是相同的,因此在Sync
这一层就提供了具体的实现,而没有下放给子类来实现 - 条件变量的支持:
Sync
实现了AQS中的isHeldExclusively
方法(该方法会被AQS中的ConditionObject
的signal方法调用),并提供了newCondition
方法创建条件变量 - 获取锁信息的方法:
Sync
提供了getOwner
、getHoldCount
、isLocked
三个方法用于获取锁的信息,外围类ReentrantLock
的三个同名方法会委托这三个方法来执行
获取锁
要利用AQS实现获取锁的功能,需要实现tryAcquire
方法。但是由于公平模式和非公平模式下获取锁的逻辑不同,因此tryAcquire
交给两个子类去实现,Sync
并不实现
但是对于非公平获取锁的模式,NonFairSync
子类实现的tryAcquire
方法实际上委托了Sync
类的nonfairTryAcquire
方法来处理。nonfairTryAcquire
源码分析放在后面的非公平模式去讲解
释放锁
要利用AQS实现释放锁的功能,需要实现tryRelease
方法。不同于获取锁,对于公平模式和非公平模式来说,释放锁的逻辑是相同的,因此tryRelease
的实现直接交给Sync
这一层来实现,而没有下放给子类来实现
tryRelease
是尝试释放资源,而在ReentrantLock
中的语义环境下就是尝试释放锁。其源码如下:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 如果当前线程不持有锁就去释放锁,会抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果释放锁后发现锁空闲
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free; // 返回锁是否空闲,如果空闲则为true
}
tryRelease
的releases
参数说明:
由于每次只释放一个锁,所以调用lock
释放锁时tryRelease
的releases
参数恒为1
但是ReentrantLock
支持条件变量,条件变量的await
方法也会调用tryRelease
方法一次性释放所有的锁资源,此时tryRelease
的参数releases
不一定为1
AQS中的release
方法会调用tryRelease
方法并接收其返回值,如下:
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
返回true,说明锁为空闲,那么就需要唤醒等待获取锁而阻塞,且等待最久的线程,让它来获取锁。因此release
会唤醒同步队列的队首线程。如果锁不是空闲,就不需要唤醒任何线程
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
非公平锁
非公平锁是借助Sync
和其子类NonfairSync
来实现的,实现了Sync
定义的lock
抽象方法,以及实现了AQS中的tryAcquire
方法以获取锁。源码如下:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
// 实现了Sync中定义的lock方法
final void lock() {
// 上来就CAS,一点也不客气————非公平性
if (compareAndSetState(0, 1)) // 如果state为0,说明锁是空闲的,直接CAS获取
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 如果锁不空闲,或者CAS竞争失败,就调用acquire去获取1个锁,可能会被阻塞
}
// 非公平竞争锁,实际上委托Sync.nonfairTryAcquire来执行
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
lock
方法先直接CAS修改state
,如果锁空闲且修改成功,则说明获取到了锁,这里也体现出非公平性,因为它不会谦让已经在同步队列中等待的线程
如果锁非空闲或者竞争失败,则会调用acquire
方法。acquire
会调用非公平锁实现的tryAcquire
方法,再次进行竞争,可能直接获取到锁,也可能再次失败,进入同步队列阻塞等待,这里同样体现了非公平性
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
公平锁
公平锁是借助Sync
和其子类FairSync
来实现的,实现了Sync
定义的lock
抽象方法,以及实现了AQS中的tryAcquire
方法以获取锁。源码如下:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
// 公平竞争锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 先检查有无排队等待的线程,如果有就不去CAS竞争——公平性
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 可重入,与非公平是一样的
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
lock
方法直接调用acquire
方法获取锁,而acquire
会调用非公平锁实现的tryAcquire
方法,而tryAcquire
也遵循公平性,因此该lock方法整体上就是公平的
tryAcquire
方法会检查锁是否空闲,如果空闲,也不会立即去CAS争夺,而是调用AQS的hasQueuedPredecessors
方法检查是否有线程在同步队列中等待,如果没有才会CAS竞争。如果有就说明不能竞争,返回false
AQS中的hasQueuedPredecessors
方法会检查是否有线程在同步队列中等待,源码如下:
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// 如果head等于tail,说明是空队列
// 如果队首的thread域不是当前线程,说明有别的线程先于当前线程等待获取锁
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
要使用公平模式的锁,需要将`ReentrantLock`的构造参数`fair`设为true:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
Lock接口的实现
ReentrantLock
实现了Lock
接口的所有方法,如下:
public void lock() {
sync.lock();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
可以看到,ReentrantLock
实现的所有Lock
方法其实都是委托给了Sync
(AQS)来执行
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
相关面试题
ReentrantLock
是如何实现可重入的
无论是公平锁还是非公平锁,获取锁调用tryAcquire
方法时,获取成功后都会设置当前持有锁的线程是自己。如果再次获取该锁,当发现锁已经被持有时,会判断持有锁的线程是否是自己,如果是就可以不用竞争而直接获取锁
简述非公平锁和公平锁之间的区别
从定义角度来说:
- 公平锁:获取锁的顺序和请求锁的顺序是一致的,即谁申请得早(等待得久),谁就最先获取锁
- 非公平锁:竞争锁时,等待时间最长的线程和刚刚过来竞争锁(不在阻塞同步队列中)的线程都有可能获取锁,CPU时间片轮询到哪个线程,哪个就能获得锁
从源码角度来说:
当锁被占用时,请求锁的所有线程都会按照FIFO的顺序在同步队列中阻塞等待。在锁被释放的时候,如果是非公平锁,则队首线程和刚刚过来请求锁而不在阻塞队列中的线程,都可能获得锁。如果是公平锁,就一定是队首线程获得锁,刚刚过来请求锁得线程会被加入同步队列阻塞等待
从效率上来说:公平锁效率低于非公平锁,主要是两方面的开销
- 代码执行上的开销:公平模式下会多执行一个方法,该方法用于判断是否有其他线程正在同步队列中等待
- 系统层面的开销:公平模式下,队首线程是阻塞的,所以必须先将队首线程唤醒,这涉及到操作系统上下文切换的操作,开销较大。而在非公平模式下,可能是刚刚过来请求锁的线程获得锁,而该线程已经是唤醒状态,不需要上下文切换
为什么
ReentrantLock.lock
方法不能被其他线程中断
因为lock
方法调用的是AQS中的acquire
方法,该方法忽略中断。而acquire
方法又会调用acquireQueued
方法,该方法执行过程中如果有其他线程中断了当前线程,只会将中断记录下来,不会响应中断。如果锁已经被获取,那么该线程需要被阻塞,阻塞调用的是LockSupport.unpark
方法,该方法接收到中断信号后,不会抛出中断异常,而是返回。返回之后又会进入acquireQueued
的循环,如果不是队首,就重新被阻塞。所以整个过程都不会被其他线程中断,只会将中断记录下来
ReentrantLock
与synchronized
之间的相同和不同点
相同点:
它们都是通过加锁实现同步,而且都是阻塞式同步,而不是非阻塞式,即当一个线程获取锁后,其他线程再请求锁就会失败而被阻塞,等到锁释放才有机会被唤醒
不同点:
ReentrantLock
:是Java 5之后提供的API层面的互斥锁;需要lock
、unlock
配合try、finally使用;支持定时获取锁功能;支持可中断的加锁方法lockInterruptibly
,在等待获取锁时响应中断,会抛出中断异常synchronized
:是Java语言的关键字,通过JVM实现;使用便捷;不支持定时获取锁功能;synchronized
在等待获取锁时不响应中断,不抛出中断异常,只记录中断状态
公平锁和非公平锁的区别在哪里?
- 公平锁:先到临界区的线程一定会比后到的先获得锁
- 非公平锁:先到临界区的线程不一定比后到的先获得锁
synchronized
加锁是公平锁还是非公平锁?
synchronized
是非公平锁,线程到达临界区就直接CAS尝试获取锁,如果失败则升级为轻量级锁,再不断CAS请求锁。当CAS失败到达一定次数之后,升级为重量级锁,放入monitor对象的队列中阻塞等待。而且入队之前也会先尝试获取锁,获取不到才进入等待队列
因此,线程获取synchronized
锁都不会关心有没有其他线程之前获取过,所以synchronized
是非公平锁
为什么要设置前驱节点的状态为
SIGNAL
?
为了表示前驱节点的后继节点对应的线程需要被唤醒,就这么简单
ReentrantLock可重入锁——源码详解的更多相关文章
- 【分布式锁】06-Zookeeper实现分布式锁:可重入锁源码分析
前言 前面已经讲解了Redis的客户端Redission是怎么实现分布式锁的,大多都深入到源码级别. 在分布式系统中,常见的分布式锁实现方案还有Zookeeper,接下来会深入研究Zookeeper是 ...
- ReentrantLock可重入锁的理解和源码简单分析
import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; /** * @author ...
- ReenTrantLock可重入锁(和synchronized的区别)总结
ReenTrantLock可重入锁(和synchronized的区别)总结 可重入性: 从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也 ...
- ReenTrantLock可重入锁和synchronized的区别
ReenTrantLock可重入锁和synchronized的区别 可重入性: 从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入 ...
- JUC 一 ReentrantLock 可重入锁
java.util.concurrent.locks ReentrantLock即可重入锁,实现了Lock和Serializable接口 ReentrantLock和synchronized都是可重入 ...
- 源码详解系列(六) ------ 全面讲解druid的使用和源码
简介 druid是用于创建和管理连接,利用"池"的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制.连接可靠性测试.连接泄露控制.缓存语句等功能,另外,druid还扩展 ...
- 源码详解系列(八) ------ 全面讲解HikariCP的使用和源码
简介 HikariCP 是用于创建和管理连接,利用"池"的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制.连接可靠性测试.连接泄露控制.缓存语句等功能,另外,和 dr ...
- RocketMQ源码详解 | Producer篇 · 其二:消息组成、发送链路
概述 在上一节 RocketMQ源码详解 | Producer篇 · 其一:Start,然后 Send 一条消息 中,我们了解了 Producer 在发送消息的流程.这次我们再来具体下看消息的构成与其 ...
- RocketMQ源码详解 | Broker篇 · 其一:线程模型与接收链路
概述 在上一节 RocketMQ源码详解 | Producer篇 · 其二:消息组成.发送链路 中,我们终于将消息发送出了 Producer,在短暂的 tcp 握手后,很快它就会进入目的 Broker ...
随机推荐
- Codeforces 1375F - Integer Game(交互)
Codeforces 题面传送门 & 洛谷题面传送门 一个奇怪的做法. 首先我们猜测答案总是 First.考虑什么样的情况能够一步把对方一步干掉.方便起见我们假设 \(a<b<c\ ...
- Codeforces 1503E - 2-Coloring(组合数学)
Codeforces 题目传送门 & 洛谷题目传送门 考虑什么样的 2-染色方式是符合题目要求的,首先蓝.黄颜色所形成的连通块个数必须 \(\le 2\),否则一定不合法,而显然如果两种颜色连 ...
- 【豆科基因组】绿豆Mungbean, Vigna radiata苏绿基因组预印
目录 一.来源 二.结果 测序组装 组装评价 编码基因预测 基因功能注释 非编码RNA注释 假基因预测 重复序列注释 进化分析和分歧时间估计 全基因组复制 LTR插入时间估计 正选择基因 一.来源 H ...
- 用Rsync实现windows下同步linux服务器的数据
一:环境 1.服务端:Red Hat Enterprise Linux Server release 6.4 (Santiago) 2.客户端:windows7旗舰版64位 3.同步对象:测试数据 4 ...
- pyyaml模块
pyyaml模块是一种文件数据处理格式的方法,常用与生成.解析或修改.yaml配置文件 1.常见.yaml文件格式内容如下 languages: - Ruby - Perl - Python webs ...
- 基于tp5免费开源的后台管理系统
基于tp5免费开源的后台管理系统 可以自定义后台菜单,模块等. 后台模板用的是:AdminLTE 简单的后台基础管理系统,有兴趣开源看看 代码地址:https://github.com/mengzhi ...
- MongoDB的搭建、参数
Mongodb官网:https://www.mongodb.com/ mkdir -r /data/db touch /data/log tar -zxvf mongodb-linux-x86_6 ...
- 阿里云ECS磁盘性能测试
阿里官方给出的性能指标 顺序读 测试命令 fio -directory=/var/lib/data -direct=1 -iodepth=1 -thread -ioengine=libaio -ran ...
- day19 进程管理
day19 进程管理 什么是进程,什么是线程 1.什么是程序 一般情况下,代码,安装包等全部都是应用程序 2.什么是进程 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进 ...
- Hive(五)【DQL数据查询】
目录 一. 基本查询 1.1 算数运算符 1.2 常用聚合函数 1.3 limit 1.4 where 1.5 比较运算符(between|in|is null) 1.6 LIKE和RLIKE 1.7 ...