ReentrantLock之公平锁源码分析
本文分析的ReentrantLock所对应的Java版本为JDK8。
在阅读本文前,读者应该知道什么是CAS、自旋。
本文大纲
1.ReentrantLock公平锁简介
2.AQS
3.lock方法
4.unlock方法
1. ReentrantLock公平锁简介
ReentrantLock是JUC(java.util.concurrent)包中Lock接口的一个实现类,它是基于AbstractQueuedSynchronizer(下文简称AQS)来实现锁的功能。ReentrantLock的内部类Sync继承了AbstractQueuedSynchronizer,Sync又有FairSync和NonFairSync两个子类。FairSync实现了公平锁相关的操作,NonFairSync实现了非公平锁相关的操作。它们之间的关系如下:

公平锁的公平之处主要体现在,对于一个新来的线程,如果锁没有被占用,它会判断等待队列中是否还有其它的等待线程,如果有的话,就加入等待队列队尾,否则就去抢占锁。
下面这段代码展示了公平锁的使用方法:
private final Lock lock = new ReentrantLock(true); // 参数true代表创建公平锁
public void method() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock();
}
}
2. AQS
下面简单介绍一下AQS中的Node内部类和几个重要的成员变量。
2.1 Node
AQS中,维护了一个Node内部类,用于包装我们的线程。我们需要关注Node中的如下属性:
- pre:当前节点的前驱节点。
- next:当前节点的后继节点。
- thread:thread表示被包装的线程。
- waitStatus:waitStatus是一个int整型,可以被赋予如下几种值:
static final int CANCELLED = 1; // 线程被取消
static final int SIGNAL = -1; // 后继节点中的线程需要被唤醒
static final int CONDITION = -2; // 暂不关注
static final int PROPAGATE = -3; // 暂不关注
另外,当一个新的Node被创建时,waitStatus是0。
2.2 head
head指向队列中的队首元素,可以理解为当前持有锁的线程。
2.3 tail
tail指向队列中的队尾元素。
2.4 state
state表示在ReentrantLock中可以理解为锁的状态,0表示当前锁没有被占用,大于0的数表示锁被当前线程重入的次数。例如,当state等于2时,表示当前线程在这把锁上进入了两次。
2.5 exclusiveOwnerThread
表示当前占用锁的线程。
2.6 等待队列
下图简单展示了AQS中的等待队列:

3. lock方法
有了上面的AQS的基础知识后,我们就可以展开对ReentrantLock公平锁的分析了,先从lock方法入手。
ReentrantLock中的lock方法很简单,只是调用了Sync类(本文研究公平锁,所以应该是FairSync类)中的lock方法:
public void lock() {
sync.lock();
}
我们跟到FairSync的lock方法,代码也很简单,调用了AQS中的acquire方法:
final void lock() {
acquire(1);
}
acquire方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 调用tryAcquire尝试去获取锁,如果获取成功,则方法结束
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 如果获取锁失败,执行acquireQueued方法,将把当前线程排入队尾
selfInterrupt();
}
tryAcquire方法:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 获取锁的状态
if (c == 0) { // 如果状态是0,表示锁没有被占用
if (!hasQueuedPredecessors() && // 判断是队列中是否有排队中的线程
compareAndSetState(0, acquires)) { // 队列中没有排队的线程,则尝试用CAS去获取一下锁
setExclusiveOwnerThread(current); // 获取锁成功,则将当前占有锁的线程设置为当前线程
return true;
}
}
// 锁被占用、队列中有排队的线程或者当前线程在获取锁的时候失败将执行下面的代码
else if (current == getExclusiveOwnerThread()) { // 当前线程是否是占有锁的线程
int nextc = c + acquires; // 是的话,表示当前线程是重入这把锁,将锁的状态进行加1
if (nextc < 0)
throw new Error("Maximum lock count exceeded"); // 锁的重入次数超过int能够表示最大的值,抛出异常
setState(nextc); // 设置锁的状态
return true;
}
return false; // 没有获取到锁
}
hasQueuedPredecessors方法:
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t && // 队列中的队首和队尾元素不相同
((s = h.next) == null || s.thread != Thread.currentThread()); // 队列中的第二个元素不为null,且第二个元素中的线程不是当前线程。这里如果返回true,说明队列中至少存在tail、head两个节点,就会执行acquireQueued将当前线程加入队尾
}
如果tryAcquire没有获取到锁,将执行:
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
我们先分析addWaiter方法:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); // 将当前线程包装成Node,mode参数值为null,表示独占模式
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred; // 如果队列中的尾节点不为空,将当前node的前驱节点设置为之前队列中的tail
if (compareAndSetTail(pred, node)) { // 用CAS把当前node设置为队尾元素
pred.next = node; // 成功的话,则将之前队尾元素的后继节点设置为当前节点。如果这里不清楚的话,请结合前面讲等待队列的那张图进行理解。
return node;
}
}
enq(node); // 队尾节点为空,或者用CAS设置队尾元素失败,则用自旋的方式入队
return node;
}
enq方法:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
if (compareAndSetHead(new Node())) // 队尾元素为空,创建一个空的Node,并设置为队首
tail = head; // 设置队首和队尾为同一个空Node,进入下一次循环
} else {
node.prev = t; // 如果队列中的尾节点不为空,将当前node的前驱节点设置为之前队列中的tail
if (compareAndSetTail(t, node)) { // 用CAS把当前node设置为队尾元素
t.next = node; // 成功的话,则将之前队尾元素的后继节点设置为当前节点
return t;
}
}
}
}
下面这张图反应了上面enq方法的处理流程:

经过上面的方法,当前node已经加入等待队列的队尾,接下来将执行acquireQueued方法:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor(); // 获取node的前驱节点
if (p == head && tryAcquire(arg)) { // 如果node的前驱是head,它将去尝试获取锁(tryAcquire方法在前面已经分析过)
setHead(node); // 获取成功,则将node设置为head
p.next = null; // 将之前的head的后继节点置空
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && // 当前node的前驱不是head,将为当前node找到一个能够将其唤醒的前驱节点;或者当前node的前驱是head,但是获取锁失败
parkAndCheckInterrupt()) // 将当前线程挂起
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire方法的作用就是找到一个能够唤醒当前node的节点:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 开始时是0
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true; // 前驱节点的状态是-1,会唤醒后继节点,可以将线程挂起
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev; // 前驱节点中的线程被取消,那就需要一直循环直到找到一个没有被设置为取消状态的前驱节点
} while (pred.waitStatus > 0);
pred.next = node; // 从后向前找,将第一个非取消状态的节点,设置这个节点的后继节点设置为当前node
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // waitStatus是0或者-3的时候,这时waitStatus都将被设置为-1
// 即后继节点需要前驱节点唤醒
}
return false; // 上层代码再进行一次循环,下次进入此方法时,将进入第一个if条件
}
找到了合适的前驱节点,parkAndCheckInterrupt方法当前线程挂起:
private final boolean parkAndCheckInterrupt() { // 将线程挂起,等待前驱节点的唤醒
LockSupport.park(this);
return Thread.interrupted();
}
4. unlock方法
ReentrantLock的unlock方法调用AQS中的release方法:
public void unlock() {
sync.release(1); // 调用AQS的release方法
}
release方法:
public final boolean release(int arg) {
if (tryRelease(arg)) { // 尝试去释放锁
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 释放锁成功,head不为空,并且head的waitStatus不为0的情况下,将唤醒后继节点
return true;
}
return false;
}
tryRelease方法:
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 将锁的状态减1
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException(); // 准备释放锁的线程不是持有锁的线程,抛出异常
boolean free = false;
if (c == 0) {
free = true; // 锁的状态是0,说明不存在重入的情况了,可以直接释放了
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
锁释放成功,将唤醒后继节点,unparkSuccessor方法:
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus; // 注意,这个node是head节点
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); // 当前node的状态是小于0,将其状态设置为0
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next; // head节点的后继节点
if (s == null || s.waitStatus > 0) {
s = null; // 执行到这表示head的后继节点是1,处于取消的状态
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t; // 从等待队列的队尾向前找,找到倒序的最后一个处于非取消状态的节点
}
if (s != null)
LockSupport.unpark(s.thread); // 唤醒head后面的处于非取消状态的第一个(正序)节点
}
到此,全文结束,大家看代码的时候结合图来理解会容易很多。
ReentrantLock之公平锁源码分析的更多相关文章
- ReentrantLock 的公平锁源码分析
ReentrantLock 源码分析 以公平锁源码解析为例: 1:数据结构: 维护Sync 对象的引用: private final Sync sync; Sync对象继承 AQS, Syn ...
- ReentrantLock之非公平锁源码分析
本文分析的ReentrantLock所对应的Java版本为JDK8. 在阅读本文前,读者应该知道什么是CAS.自旋. 由于ReentrantLock的公平锁和非公平锁中有许多共同代码,本文只会对这两种 ...
- (转)ReentrantLock实现原理及源码分析
背景:ReetrantLock底层是基于AQS实现的(CAS+CHL),有公平和非公平两种区别. 这种底层机制,很有必要通过跟踪源码来进行分析. 参考 ReentrantLock实现原理及源码分析 源 ...
- Redisson分布式锁学习总结:可重入锁 RedissonLock#lock 获取锁源码分析
原文:Redisson分布式锁学习总结:可重入锁 RedissonLock#lock 获取锁源码分析 一.RedissonLock#lock 源码分析 1.根据锁key计算出 slot,一个slot对 ...
- ReentrantLock 公平锁源码 第0篇
ReentrantLock 0 关于ReentrantLock的文章其实写过的,但当时写的感觉不是太好,就给删了,那为啥又要再写一遍呢 最近闲着没事想自己写个锁,然后整了几天出来后不是跑丢线程就是和没 ...
- ReentrantLock 公平锁源码 第1篇
ReentrantLock 1 这篇还是接着ReentrantLock的公平锁,没看过第0篇的可以先去看上一篇https://www.cnblogs.com/sunankang/p/16456342. ...
- ReentrantLock实现原理及源码分析
ReentrantLock是Java并发包中提供的一个可重入的互斥锁.ReentrantLock和synchronized在基本用法,行为语义上都是类似的,同样都具有可重入性.只不过相比原生的Sync ...
- 基于ReentrantLock的AQS的源码分析(独占、非中断、不超时部分)
刚刚看完了并发实践这本书,算是理论具备了,看到了AQS的介绍,再看看源码,发现要想把并发理解透还是很难得,花了几个小时细分析了一下把可能出现的场景尽可能的往代码中去套,还是有些收获,但是真的很费脑,还 ...
- RedissonLock分布式锁源码分析
最近碰到的一个问题,Java代码中写了一个定时器,分布式部署的时候,多台同时执行的话就会出现重复的数据,为了避免这种情况,之前是通过在配置文件里写上可以执行这段代码的IP,代码中判断如果跟这个IP相等 ...
随机推荐
- 【转】H.264中的NAL技术
NAL技术 1.NAL概述 NAL全称Network Abstract Layer,即网络抽象层.在H.264/AVC视频编码标准中,整个系统框架被分为了两个层面:视频编码层面(VCL)和网络抽象层面 ...
- python3学习笔记3---引用http://python3-cookbook.readthedocs.io/zh_CN/latest/
2018-03-01数据结构和算法(3) 1.11 命名切片 假定你有一段代码要从一个记录字符串中几个固定位置提取出特定的数据字段(比如文件或类似格式): ###### 012345678901234 ...
- EF Code First 数据迁移配置
这里我想讲清楚code first 数据迁移的两种模式,还有开发环境和生产环境数据迁移的最佳实践. 1.1 数据迁移综述 EF Code first 虽然已经有了几种不同的数据库初始化策略,但是大部分 ...
- sudo pip install MySQLdb
安装数据库第三方包,报错: Could not find a version that satisfies the requirement MySQLdb (from versions: )No ma ...
- Flask类的属性和方法大全
Flask Property__class____dict____doc____module__app_ctx_globals_classconfig_classdebugdefault_config ...
- PHP 中的 __FILE__ 和__DIR__常量
__DIR__ :当前内容写在哪个文件就显示这个文件目录 __FILE__ : 当前内容写在哪个文件就显示这个文件目录+文件名 比如文件 b.php 包含如下内容: <?php $basedir ...
- php使用http_build_query,parse_url,parse_str创建与解析url详解
1.http_build_query string http_build_query ( mixed $query_data [, string $numeric_prefix [, string $ ...
- 在高分屏正确显示CHM文件
今天下了白色相簿2推,发现里面的chm格式的帮助文档显示不正确,又没法在应用程序直接设置系统分辨率托管,google了一下找到了这个方法: 新建 HKEY_LOCAL_MACHINE\ SOFTWAR ...
- Java 8 文件操作(转)
我们知道在JDK6甚至之前的时候,我们想要读取一个文本文件也是非常麻烦的一件事,而现在他们都变得简单了, 这要归功于NIO2,我们先看看之前的做法: 读取一个文本文件 BufferedReader b ...
- SSM-Spring-21:Spring中事物的使用案例
------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 股票买卖案例(我会用三种开启事物的方法 代理工厂bean版的,注解版的,aspectj xml版的) 简单的介 ...