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相等 ...
随机推荐
- ruby簡單的代碼行統計工具
看代码 # encoding: utf-8 class CodeLineStat attr_reader :code_lines def initialize @code_lines = 0 end ...
- jvm垃圾回收(三)
一.分代思想(年轻代.老年代.永久代): 1.一个新人(new对象)会优先在伊甸园(Eden区)出生,当伊甸园(Eden区)人口达到最大容量时,JVM会派MinorGC去看看哪些人还有价值 2.伊甸园 ...
- JS跨域:1.解决方案之-SpringMVC拦截器
一 拦截器代码 package com.wiimedia.controller; import java.util.List; import javax.servlet.http.HttpServle ...
- golang自定义路由控制实现(一)
由于本人之前一直是Java Coder,在Java web开发中其实大家都很依赖框架,所以当在学习Golang的时候,自己便想着在Go开发中脱离框架,自己动手造框架来练习.通过学习借鉴Java ...
- 万网主机使用wordpress发送邮件的方法
今天弄了一下午总算明白了,这里写一下具体过程. 首先是邮箱,万网主机是不支持mail()函数的,所以默认的不可用,如果你想发送邮件的话,只能使用fsockopen()函数.首先进入万网主机管理平台,启 ...
- Python 内置的一些高效率函数用法
1. filter(function,sequence) 将sequence中的每个元素,依次传进function函数(可以自定义,返回的结果是True或者False)筛选,返回符合条件的元素,重组 ...
- Net Core 生成图形验证码
1. NetCore ZKweb 在我第一次绘制图形验证码时是采用的ZKweb的绘制库,奉上代码参考 public byte[] GetVerifyCode(out string ...
- Python_marshal模块操作二进制文件
import marshal #导入模块 x1=30 #待序列化的对象 x2=5.0 x3=[1,2,3] x4=(4,5,6) x5={'a':1,'b':2,'c':3} x6={7,8,9} x ...
- SSH X11 MAC
1. X11 for Mac 2. Ubuntu下通过SSH转发X窗口需要具备的条件 原文:http://unix.stackexchange.com/questions/12755/how- ...
- System.nanoTime理解
JDK1.5之后java中的计时给出了更精确的方法:System.nanoTime(),输出的精度是纳秒级别,这个给一些性能测试提供了更准确的参考. 但是这个方法有个需要注意的地方,不能用来计算今天是 ...