前言

AQS即AbstractQueuedSynchronizer,是JUC包中的一个核心抽象类,JUC包中的绝大多数功能都是直接或间接通过它来实现的。本文是AQS系列的第一篇,后面会持续更新多篇,争取将JUC包中AQS相关的常用功能讲清楚,一方面巩固自己的知识体系,一方面亦可与各位园友互相学习。寒冷的冬天,要用技术来温暖自己。

一、AQS与ReentrantLock的关系

先奉上一张自制的丑陋类图

从下往上看,ReentrantLock类内部有两个静态内部类FairSync和NonfairSync,分别代表了公平锁和非公平锁(注意ReentrantLock实现的锁是可重入排它锁)。这两个静态内部类又共同继承了ReentrantLock的一个内部静态抽象类Sync,此抽象类继承AQS。

类的关系搞清楚了,我们下面一起看一下源码。

二、源码解读

ReentrantLock的默认构造方法创建的是非公平锁,也可以通过传入true来指定生成公平锁。下面我们以公平锁的加锁过程为例,进行解读源码。在解读源码之前需要先明确一下AQS中的state属性,它是int类型,state=0表示当前lock没有被占用,state=1表示被占用,如果是重入状态,则重入了几次state就是几。

 public class JucLockDemo1 {
public static void main(String[] args){
ReentrantLock lock = new ReentrantLock(true);
Thread t1 = new Thread(() -> {
lock.lock();
// 业务逻辑
lock.unlock();
});
t1.start();
System.out.println("main end");
}
}

其中第5行lock方法点进去的代码:

 public void lock() {
sync.lock();
}

直接调了sync的lock方法,sync下面的lock方法是抽象方法,方法逻辑取决于具体的实现类,因为我们这里创建的是公平锁,所以进FairSync看它的lock方法实现:

 final void lock() {
acquire(1);
}

FairSync中的lock方法很简单,直接调用了acquire方法,参数是1,继续跟踪:

 public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

acquire方法位于AQS中,很重要,虽然只有短短的三行,但是里面的内容非常多。下面对里面的方法分别进行解读。

方法1:tryAcquire(arg)

此方法在FairSync中进行了实现,代码如下所示:

 protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 判断state状态,如果是0表示锁空闲,可以去尝试获取
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}// exclusiceOwnerThread存放的是当前运行的独占线程,如果此处判断为true,说明是当前线程第二次加锁,可以重入,只是要将state+1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

第二个if判断很好理解,是ReentrantLock对重入和排他的支持(所以说它是可重入排他锁),但是判断c==0之后的逻辑就比较麻烦了。

首先理解一下当前的逻辑:如果state=0说明lock空闲,又因为是公平锁,所以要先判断当前AQS队列中还有没有排队的任务,如果没有的话,就走一个CAS将state改成1,然后设置排他的执行线程,获取执行权;如果队列中有任务,那么acquire方法只能先返回false了。那么可以推断出,hasQueuedPredecessors方法就是用来判断队列中是否有排队的

点进去看看Lea大神的实现逻辑吧。

 public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

代码不多,但表达的意思比较晦涩。第一个判断h!=t,如果h=t,说明队列是空的,这时这个判断条件是false,方法直接就返回了,这时外面的if取反是true,会继续走CAS抢占state和排他线程,获取锁,这种情况的路就走完了。如果h!=t为true,说明现在队列中有任务,这时进入后面的大括号 ((s = h.next) == null || s.thread != Thread.currentThread()) ,在队列中有任务的情况下,还有两种可能,一种是队列中的第一个任务就是当前线程,另一种是第一个任务不是当前线程。因为是公平锁,如果第一个任务时当前线程的话,那么它有权再去申请一下获取锁,如果第一个任务不是当前线程,那么当前线程就乖乖排队吧,等前面的执行完了才能轮到你。后面的大括号就是对这两种情况进行了区分,我们用反向逻辑来分析。方法hasQueuedPredecessors表示如果当前线程可以去竞争锁则返回false,不能竞争锁则返回true后面大括号结果为false的话当前线程才会去抢占锁,一个或运算怎样才能是false?或的两边都是false,就是说要(s = h.next) != null && s.thread == Thread.currentThread(),意思就是队列中第一个任务不为空且第一个任务就是当前线程,而这个&&的非与上述源码中的||在逻辑上是等价的,所以到这里意思就清楚了,return的&&连接的两个条件意思是:判断是否队列不为空且(第一个任务为空或者不是当前线程)。

hasQueuedPredecessors方法讲完,tryAcquire方法就没有什么难点了,这时我们回到上面开始的acquire(int arg)方法。如果tryAcquire返回的是true,说明获取到了锁,那么就不会再走后面的流程了;如果返回的是false,则进入acquireQueue。但我们先看里面的addWaiter方法。

方法2:  addWaiter(Node.EXCLUSIVE), arg)

此方法用于生成当前线程的node节点并把它放在队尾,方法源码:

 private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);// 创建当前线程的node节点
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) { // 判断队尾是否为空,如果不为空则将node节点拼接在后面
node.prev = pred; // 将node节点连接到队尾节点
if (compareAndSetTail(pred, node)) { // 通过CAS将node节点放到队尾
pred.next = node; // 如果CAS操作成功了,那么将原队尾节点的next连接到node节点,组成双向队列
return node;
}
}
enq(node); // 能到这里的话分两种情况:1、队尾是空的;2、队尾不是空的,但是进行CAS操作时由于被其他线程抢占导致失败;
return node;
}

通过注解大家应该能梳理清楚逻辑,下面着重说一下enq(node)方法的实现:

 private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize 队尾是null,符合前面说的第一种情况
if (compareAndSetHead(new Node())) // 设置队首
tail = head; // 队首队尾都初始化成空node
} else { // 队尾不为空,是前面说的第二种情况,此种情况的处理逻辑同上面对pred != null的处理
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

可以看到此方法无限循环,直到执行完else中的逻辑。此处需要注意的一点是,如果刚开始时队列是空的,即tail是null,会触发队首队尾的初始化,初始化之后再一次循环会进入else中,将node放到原队尾的后面,返回t。注意返回的t没有用到,是在其他场景的方法中用的。

 方法3:acquireQueued(final Node node, int arg)

该方法用于获取锁,返回值表示当前获取到锁的线程在获取锁的过程中是否中断过,下面先看源码:

 final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor(); // 获取当前节点的前一个节点
if (p == head && tryAcquire(arg)) { // 如果p==head说明node是第一个任务,那么就可以通过tryAcquire去获取锁
setHead(node); // 获取锁成功,则将node放到队首位置,并将thread和prev置为null
p.next = null; // help GC 再将p的next置为null,切断与外界的一切联系
failed = false;
return interrupted;
}// 下面if中的两个方法很重要,着重讲解
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

通过注解,相信对第一个if中的逻辑能理解清楚,下我们着重讲解第二个if中的两个方法。

第一个是 shouldParkAfterFailedAcquire(p, node) 方法,此方法的逻辑为:

 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 1、对于新建的Node节点,此状态都为0(只有addConditionWaiter新建node节点时才不是0)
if (ws == Node.SIGNAL)
// 3、在2中将ws置为-1后,该方法返回false,外层for循环再走一圈,第二次进入此方法时会进入这里,直接返回true。 -1的状态表示可以将当前线程park
return true;
if (ws > 0) { do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 2、是ws=0的话会进入这里,将ws置为-1,0的状态表示还不能park
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}

如果返回的是true,则进入第二个方法将当前线程暂停:

 private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}

当前面的线程执行完毕,唤醒这个线程的时候,就会从第三行开始继续执行for循环中获取锁的逻辑,直到获取锁。

到这里,ReentrantLock的lock方法便结束了,整体流程就是这样。看JUC包中的源码,可以看到写的很简洁,有时一两个简单的判断条件却代表了非常多的意思,充分显示了编程者缜密又举重若轻的实力,读这样的源码,有一种看本格推理小说般的思维上的愉悦感。

下一节我们将介绍unlock方法的原理,与本节最后一个方法就能接上了,下期再会!

AQS系列(一)- ReentrantLock的加锁的更多相关文章

  1. AQS系列(三)- ReentrantReadWriteLock读写锁的加锁

    前言 前两篇我们讲述了ReentrantLock的加锁释放锁过程,相对而言比较简单,本篇进入深水区,看看ReentrantReadWriteLock-读写锁的加锁过程是如何实现的,继续拜读老Lea凌厉 ...

  2. AQS系列(二)- ReentrantLock的释放锁

    前言 在AQS系列(一)中我们一起看了ReentrantLock加锁的过程,今天我们看释放锁,看看老Lea那冷峻的思维是如何在代码中笔走龙蛇的. 正文 追踪unlock方法: public void ...

  3. Java并发系列[5]----ReentrantLock源码分析

    在Java5.0之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile.我们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可 ...

  4. 死磕 java同步系列之ReentrantLock VS synchronized——结果可能跟你想的不一样

    问题 (1)ReentrantLock有哪些优点? (2)ReentrantLock有哪些缺点? (3)ReentrantLock是否可以完全替代synchronized? 简介 synchroniz ...

  5. Java并发编程锁系列之ReentrantLock对象总结

    Java并发编程锁系列之ReentrantLock对象总结 在Java并发编程中,根据不同维度来区分锁的话,锁可以分为十五种.ReentranckLock就是其中的多个分类. 本文主要内容:重入锁理解 ...

  6. AQS系列(七)- 终篇:AQS总结

    前言 本文是对之前AQS系列文章的一个小结,首先看看以下几个问题: 1.ReentrantLock和ReentrantReadWriteLock的可重入特性是如何实现的? 2.哪个变量控制着锁是否被占 ...

  7. 深入理解Java并发框架AQS系列(一):线程

    深入理解Java并发框架AQS系列(一):线程 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念 一.概述 1.1.前言 重剑无锋,大巧不工 读j.u.c包下的源码,永远无法绕开的经典 ...

  8. 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念

    深入理解Java并发框架AQS系列(一):线程 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念 一.AQS框架简介 AQS诞生于Jdk1.5,在当时低效且功能单一的synchroni ...

  9. 死磕 java同步系列之ReentrantLock源码解析(二)——条件锁

    问题 (1)条件锁是什么? (2)条件锁适用于什么场景? (3)条件锁的await()是在其它线程signal()的时候唤醒的吗? 简介 条件锁,是指在获取锁之后发现当前业务场景自己无法处理,而需要等 ...

随机推荐

  1. Ubuntu 10.04——boa服务器的搭建

     声明:自从第一次发表博文不知不觉过去了好久了,非常抱歉没能把自己的东西分享出来,但是由于上家公司本月初裁员,所以致使学的新东西成了半成品,无奈又换了一家,目前已工作三周了,自己也很想写博文分享知识, ...

  2. [考试反思]1005csp-s模拟测试60:招魂

    最近总是好一场烂一场的.没有连续两场排名波动小于20的... 没人管.反正大脸一点脸没有就又AK了. 但是T3爆零这种事情吧... 爆搜不是很难打,但是想优化想了半天剩的时间不够结果赶忙打出来了,然后 ...

  3. IoTClient开发4 - ModBusTcp协议服务端模拟

    前言 上篇我们实现了ModBusTcp协议的客户端读写,可是在很多时候编写业务代码之前是没有现场环境的.总不能在客户现场去写代码,或是蒙着眼睛写然后求神拜佛不出错,又或是在办公室部署一套硬件环境.怎么 ...

  4. What's your name?

    Hello. My name is james. What's your name? Hi, I'm Jessica. Nice to meet you. Nice to meet you, too. ...

  5. 来了!GitHub for mobile 发布!iOS beta 版已来,Android 版即将发布

    北京时间 2019 年 11 月 14 日,在 GitHub Universe 2019大会上,GitHub 正式发布了 GitHub for mobile,支持 iOS 与 Android 两大移动 ...

  6. JavaScript with Image:创建缩略图

    当图片很大,直接把图片从Server下载到浏览器上看是一种很不明智的做法,浪费了服务器的资源,网络带宽和客户端的资源.所以,通常Server和Client之间会传输缩略图,只有当Client请求某张图 ...

  7. NW.js打包一个桌面应用

    1.安装nw(可以到官网:https://nwjs.io下载) npm install nw -g 2.创建一个最最简单的nw应用 在nwjs文件夹中 新建index.html和package.jso ...

  8. Intellij IDEA搭建JSP+Tomcat开发环境

    1.新建项目 然后填入项目名称和选择项目路径,填完点击完成. 2.添加WEB框架 别问我为什么不一开始就直接新建WEB框架,因为我也是看的别人的教程0.0 不过还遇到了一些新问题,后面会讲到 3.配置 ...

  9. Maven系列第8篇:你的maven项目构建太慢了,我实在看不下去,带你一起磨刀!!多数使用maven的人都经常想要的一种功能,但是大多数人都不知道如何使用!!!

    maven系列目标:从入门开始开始掌握一个高级开发所需要的maven技能. 这是maven系列第8篇. 整个maven系列的内容前后是有依赖的,如果之前没有接触过maven,建议从第一篇看起,本文尾部 ...

  10. hdu 1068 Girls and Boys (最大独立集)

    Girls and BoysTime Limit: 20000/10000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)T ...