ReentrantLock源码探究1:非公平锁的获取和释放
AQS简单介绍
Sync是ReentrantLock的一个内部类,它继承了AbstractQueuedSynchronizer,即AQS,在CountDownLatch、FutureTask、Semaphore、ReentrantLock等源码中,我们都能看到它们的身影,足见其重要性。此处我们需要先了解下AQS才能更愉悦地阅读源码。
AQS中是基于FIFO队列的实现,那么它必然包含队列中元素的定义,在这里它是Node:
属 性 | 定 义 |
---|---|
Node SHARED = new Node() | 表示Node处于共享模式 |
Node EXCLUSIVE = null | 表示Node处于独占模式 |
int CANCELLED = 1 | 因为超时或者中断,Node被设置为取消状态,被取消的Node不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态,处于这种状态的Node会被踢出队列,被GC回收 |
int SIGNAL = -1 | 表示这个Node的继任Node被阻塞了,到时需要通知它 |
int CONDITION = -2 | 表示这个Node在条件队列中,因为等待某个条件而被阻塞 |
int PROPAGATE = -3 | 使用在共享模式头Node有可能处于这种状态, 表示锁的下一次获取可以无条件传播 |
int waitStatus | 0,新Node会处于这种状态 |
Node prev | 队列中某个Node的前驱Node |
Node next | 队列中某个Node的后继Node |
Thread thread | 这个Node持有的线程,表示等待锁的线程 |
Node nextWaiter | 表示下一个等待condition的Node |
AQS中包含的方法有
属性/方法 | 含 义 |
---|---|
Thread exclusiveOwnerThread | 这个是AQS父类AbstractOwnableSynchronizer的属性,表示独占模式同步器的当前拥有者 |
Node | 上面已经介绍过了,FIFO队列的基本单位 |
Node head | FIFO队列中的头Node |
Node tail | FIFO队列中的尾Node |
int state | 同步状态,0表示未锁 |
int getState() | 获取同步状态 |
setState(int newState) | 设置同步状态 |
boolean compareAndSetState(int expect, int update) | 利用CAS进行State的设置 |
long spinForTimeoutThreshold = 1000L | 线程自旋等待的时间 |
Node enq(final Node node) | 插入一个Node到FIFO队列中 |
Node addWaiter(Node mode) | 为当前线程和指定模式创建并扩充一个等待队列 |
void setHead(Node node) | 设置队列的头Node |
void unparkSuccessor(Node node) | 如果存在的话,唤起Node持有的线程 |
void doReleaseShared() | 共享模式下做释放锁的动作 |
void cancelAcquire(Node node) | 取消正在进行的Node获取锁的尝试 |
boolean shouldParkAfterFailedAcquire(Node pred, Node node) | 在尝试获取锁失败后是否应该禁用当前线程并等待 |
void selfInterrupt() | 中断当前线程本身 |
boolean parkAndCheckInterrupt() | 禁用当前线程进入等待状态并中断线程本身 |
boolean acquireQueued(final Node node, int arg) | 队列中的线程获取锁 |
tryAcquire(int arg) | 尝试获得锁(由AQS的子类实现它) |
tryRelease(int arg) | 尝试释放锁(由AQS的子类实现它) |
isHeldExclusively() | 是否独自持有锁 |
acquire(int arg) | 获取锁 |
release(int arg) | 释放锁 |
compareAndSetHead(Node update) | 利用CAS设置头Node |
compareAndSetTail(Node expect, Node update) | 利用CAS设置尾Node |
compareAndSetWaitStatus(Node node, int expect, int update) | 利用CAS设置某个Node中的等待状态 |
另外在源码中多处使用了CAS,有关CAS的内容,可查看:
乐观锁的一种实现方式:CAS
非公平锁的获取过程
假设有两个线程:线程1和线程2尝试获取同一个锁(非公平锁),过程如下
- 线程1调用lock方法
final void lock() {
if (compareAndSetState(0, 1)) //使用CAS将同步状态设置为1
setExclusiveOwnerThread(Thread.currentThread());//成功则设置线程1为当前锁的独占线程
else
acquire(1); //设置失败时,尝试获取锁
}
//上述代码正常情况下执行完毕后,线程1成为了独占线程。
- 线程2此时也尝试获取锁,调用lock方法,此时CAS设置时会失败,进入acquire方法。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt(); //重新获取锁失败且线程发生了中断,自行中断
}
- 这里面,会首先调用tryAcquire方法尝试再次获取锁,因为我们演示的是非公平锁,因此调用的方法是nonfairTryAcquire。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread(); //current指向当前线程2
int c = getState(); //若线程1未释放锁,则c>0,若线程1已经释放锁,则c=0
if (c == 0) { //线程1已经释放了锁
if (compareAndSetState(0, acquires)) { //使用CAS将state设置为1
setExclusiveOwnerThread(current); //并设置线程2为独占线程
return true; //返回true,获取锁成功
}
}
//判断该线程是否是重入,即之前已经获取到了锁
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; //每重入一次,将state+1。
if (nextc < 0) // overflow //state+1<0,说明原state为负数,抛出异常
throw new Error("Maximum lock count exceeded");
setState(nextc); //设置state为新值
return true; //返回true,获取重入锁成功。
}
return false; //返回flase,获取锁失败
}
- 此时线程2使用tryAcquire方法获取锁,如果也是失败,那么,会调用addWaiter(Node.EXCLUSIVE)方法
private Node addWaiter(Node mode) { //此处mode为独占模式
Node node = new Node(Thread.currentThread(), mode);//将当前线程(此处为线程2)绑定到新节点node上,并设置为独占模式
// Try the fast path of enq; backup to full enq on failure
Node pred = tail; //获取原队列的尾节点pred
if (pred != null) { //若原尾节点pred非空,则说明已经存在一个队列
node.prev = pred; //设置新节点node的前置为pred
if (compareAndSetTail(pred, node)) {//使用CAS设置新的尾节点为node
pred.next = node; //设置pred的后置为node,建立双向链接
return node; //返回node
}
}
enq(node); //进入此处说明原队列不存在,需要初始化队列
return node;
}
private Node enq(final Node node) { //此处传入的参数node是绑定了线程2的节点
for (;;) {
Node t = tail; //获取原队列的尾节点t
if (t == null) { // Must initialize //若尾节点为空,说明队列尚未形成
if (compareAndSetHead(new Node())) //设置一个空的,未绑定任何线程的节点为新队列的头节点
tail = head; //新队列只有一个节点,既是头也是尾
} else { //若t非空,说明队列已经形成
node.prev = t; //将node的前置设为t
if (compareAndSetTail(t, node)) { //CAS设置新的尾节点为node
t.next = node; //设置t的后继为node,建立双向链接
return t; //返回t
}
}
}
}
- 现在看外层方法acquireQueued,此时传入的参数node是线程2所在节点,该方法的作用是在等待队列中,当有其他线程释放了资源,那么队列中在等待的线程就可以开始行动
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; //是否获取到资源
try {
boolean interrupted = false; //等待过程中是否被中断
//自旋,维护等待队列中线程的执行。
for (;;) {
final Node p = node.predecessor(); //获取node的前置p
if (p == head && tryAcquire(arg)) { //若前置p为头结点并且重新获取锁成功
setHead(node); //设置新的头节点为node
p.next = null; // help GC //取消p和链表的链接
failed = false; //获取资源未失败
return interrupted; //等待过程未被中断
}
if (shouldParkAfterFailedAcquire(p, node) && //若前置节点是Node.SIGNAL状态
parkAndCheckInterrupt()) //将节点设置为Waitting状态
interrupted = true; //此时线程中断状态为true
}
} finally {
if (failed) //如果获取资源成功那么取消获取过程
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; //获取前置节点的等待状态
if (ws == Node.SIGNAL) //Node.SIGNAL表示继任者线程需要被唤醒,那么就可以直接返回;
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) { //若ws>0,说明前驱被取消,那么执行循环往前一直查找,知道找到未被取消的,将node排在它的后面。
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else { //进入else,说明ws=0或者Node.PROPAGATE
/*
* 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.
*/
//使用CAS设置前置的节点状态为Node.SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
该部分代码可以用现实中排队办理业务的情况来说明:
假设你排队去办理业务,队伍很长,因此除了当前正在办理业务的人,其他所有排队的人都在低头玩手机,且每个排队的人有以下三种状态:①.正常排队,且办完业务后会通知后面的人别玩手机了可以开始办理业务了。②.发现队伍过长,不排队了,走了。③.正常排队,办完业务后不通知后面的人,直接走。
此时你进入该队伍的尾部开始排队。
1.第一步,判断排队在你前面的人是否会通知你,如果会通知,那么我们就可以不用关心其他问题,在队列中待着玩手机即可。
2.第二步,如果发现排在你前面的人不排队了,要走了,那么此时我们就得往前走一位,并开始不断询问前面的人是不是也准备不排队了,直到我们排在了一个确定不会走的人后面。
3.第三步,排在你前面的人不是准备走的,但是他也不会通知你,那么你就要告诉他,一定得在办完业务后通知你。
当我们确定我们已经在队列中待好后(前置会通知我们),那么我们就可以开始休息。parkAndCheckInterrupt方法让我们的线程进入等待的状态,即休息状态。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //调用park()使线程进入waiting状态
return Thread.interrupted(); //如果被唤醒,查看自己是不是被中断的。
}
锁的释放过程
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) { //若tryRelease后无人占用锁
Node h = head; //获取队列的头结点h
if (h != null && h.waitStatus != 0) //若h非空,且h的waitStatus不为0
unparkSuccessor(h); //唤醒后继
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases; //当前state-1,得到c
if (Thread.currentThread() != getExclusiveOwnerThread()) //执行releas的不是获取锁的独占线程,抛出异常
throw new IllegalMonitorStateException();
boolean free = false; //free用来标记锁是否可获取状态
if (c == 0) { //若state=0
free = true; //那么当前锁是可获取的
setExclusiveOwnerThread(null); //设置当前锁的独占线程为null
}
setState(c); //设置当前state为c
return free; //返回锁是否是可获取状态
}
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; //获取当前线程对应节点的waitStatus
if (ws < 0) //将当前线程对应节点waitStatus置为0
compareAndSetWaitStatus(node, ws, 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; //获取当前线程对应节点的后继节点s
if (s == null || s.waitStatus > 0) { //若s为空或s的状态是canceled
s = null; //将s设置为null。
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0) //此处从尾到头进行遍历,找到队列最前列的节点且状态不是Canceled,将其设置为s。但此处为何从尾部开始遍历尚未弄清楚。
s = t;
}
if (s != null) //若上述遍历找到的s非空
LockSupport.unpark(s.thread); //调用lockSupport.unpark唤醒s对应的线程
}
release方法的逻辑仍然可以用一个办理完业务的人的后续动作来进行说明:
1.若A办理业务后无其他业务需要办理,那么表示当前业务窗口是free的。
2.A将自己的等待状态置为0,相当于退出队列。然后检查自己后面的人是否是空或者取消排队的状态。若为真,将后置设为空。
3.从队列的尾部遍历到头部,直到找到队列最前头的那个,且它的等待状态不是取消状态,那么将其唤醒,告知他可以开始办理业务了。
关于源码的一点疑问
本文中部分源码本人暂时也尚未能理解,希望各位大佬不吝赐教,主要有以下一些问题:
1.在unparkSuccessor方法中,找到队列下一个节点并将其唤醒时,为什么从尾到头遍历
if (s == null || s.waitStatus > 0) { //若s为空或s的状态是canceled
s = null; //将s设置为null。
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0) //倒序遍历?
s = t;
}
2.在acquireQueued方法中,自旋结束后的finally代码块的作用。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; //是否获取到资源
try {
boolean interrupted = false; //等待过程中是否被中断
//自旋,维护等待队列中线程的执行。
for (;;) {
final Node p = node.predecessor(); //获取node的前置p
if (p == head && tryAcquire(arg)) { //若前置p为头结点并且重新获取锁成功
setHead(node); //设置新的头节点为node
p.next = null; // help GC //取消p和链表的链接
failed = false; //获取资源未失败
return interrupted; //等待过程未被中断
}
if (shouldParkAfterFailedAcquire(p, node) && //若前置节点是Node.SIGNAL状态
parkAndCheckInterrupt()) //将节点设置为Waitting状态
interrupted = true; //此时线程中断状态为true
}
} finally {
if (failed) //如果自旋结束,那么说明failed = false已经执行了,那么这个canclAcquire方法什么情况下会执行?
cancelAcquire(node);
}
}
ReentrantLock源码探究1:非公平锁的获取和释放的更多相关文章
- ReentrantLock源码探究
ReentrantLock是一种可重入锁,可重入是说同一个线程可以多次获取同一个锁,内部会有相应的字段记录重入次数,它同时也是一把互斥锁,意味着同时只有一个线程能获取到可重入锁. 1.构造函数 pub ...
- 全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(二)资源的获取和释放
上期的<全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(一)AQS基础>中介绍了什么是AQS,以及AQS的基本结构.有了这些概念做铺垫之后,我们就可以正 ...
- Java并发编程-ReentrantLock源码分析
一.前言 在分析了 AbstractQueuedSynchronier 源码后,接着分析ReentrantLock源码,其实在 AbstractQueuedSynchronizer 的分析中,已经提到 ...
- ReentrantLock源码分析--jdk1.8
JDK1.8 ArrayList源码分析--jdk1.8LinkedList源码分析--jdk1.8HashMap源码分析--jdk1.8AQS源码分析--jdk1.8ReentrantLock源码分 ...
- ReentrantLock 源码分析以及 AQS (一)
前言 JDK1.5 之后发布了JUC(java.util.concurrent),用于解决多线程并发问题.AQS 是一个特别重要的同步框架,很多同步类都借助于 AQS 实现了对线程同步状态的管理. A ...
- 死磕 java同步系列之ReentrantLock源码解析(一)——公平锁、非公平锁
问题 (1)重入锁是什么? (2)ReentrantLock如何实现重入锁? (3)ReentrantLock为什么默认是非公平模式? (4)ReentrantLock除了可重入还有哪些特性? 简介 ...
- 理解ReentrantLock的公平锁和非公平锁
学习AQS的时候,了解到AQS依赖于内部的FIFO同步队列来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个Node对象并将其加入到同步队列,同时会阻塞当 ...
- ReentrantLock 如何实现非公平锁?和公平锁实现有什么区别
reentrant 英[riːˈɛntrənt] 美[ˌriˈɛntrənt] 先学会读.单词原意是可重入的 考察显示锁的使用.可延伸知识点 独占锁 & 共享锁 独占锁 - 悲观锁(不能同时被 ...
- ReentrantLock中的公平锁与非公平锁
简介 ReentrantLock是一种可重入锁,可以等同于synchronized的使用,但是比synchronized更加的强大.灵活. 一个可重入的排他锁,它具有与使用 synchronized ...
随机推荐
- 跨平台编译ceres for Android
折腾了几乎一天,记录一下. 最大的坑是官网给出的 1.进入ceres源代码目录下的jni目录 2.EIGEN_PATH="指向eigen库目录,即包含EIGEN文件夹的那个文件夹” ndk- ...
- Linux下MySql基本操作命令
(1).切换至MySql目录下[假设MySql安装路径为:/home/mysql/bin] cd /home/mysql/bin (2).连接MySql mysql -u用户名 -p,回车后输入密码 ...
- ThreadLocal <T>类的说明 转载 原作者 lujh99
首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的.各 ...
- 5.java动态代理、反射
1.java动态代理.反射(IDEA导入JUnit4) 1.1.反射 通过反射的方式可以获取class对象中的属性.方法.构造函数等 1.2.反射代码 import java.io.Serializa ...
- deepin禁用笔记本自带键盘
参考命令: sudo apt install xinput xinput xinput list-props 'AT Translated Set 2 keyboard' xinput set-pro ...
- 自动化测试 selenium 测试软件安装
一.自动化测试优点 1.对程序的回归测试更方便.在程修改的比较平凡的时候,表现的更明显. 2.可以代替测试人员运行更繁琐的测试,也可以代替测试人员不可能完成的操作(比如连续点击50次) 3.更好的 ...
- python学习笔记(三)条件判断和循环
1.条件判断语句 Python中条件选择语句的关键字为:if .elif .else这三个.其基本形式如下: 1 2 3 4 5 6 7 8 9 age_of_cc = 27 age = int( ...
- Mybatis关联查询(转载)
原文地址: http://www.cnblogs.com/xiaolang8762400/p/7399892.html mybatis 提供了高级的关联查询功能,可以很方便地将数据库获取的结果集映 ...
- css全部理解
如何设置标签样式 给标签设置长宽 只有块儿级标签才可以设置长宽 行内标签设置了没有任何作用(仅仅只取决于内部文本值) 字体颜色 color后面可以跟多种颜色数据 颜色英文 red #06a0de 直接 ...
- Java-WEB开发常用方法整理
/** * 此类中收集Java编程中WEB开发常用到的一些工具. * 为避免生成此类的实例,构造方法被申明为private类型的. * @author */ import java.io.IOExce ...