[Java并发] AQS抽象队列同步器源码解析--独占锁获取过程

上一篇已经讲解了AQS独占锁的获取过程,接下来就是对AQS独占锁的释放过程进行详细的分析说明,废话不多说,直接进入正文...

锁释放入口release(int arg)

首先进行说明下,能够正常执行到release方法这里来的线程都是获取到锁的,从下面代码可以看出释放锁步骤只有两个重要的方法:tryRelease 与unparkSuccessor ,tryRelease尝试释放锁,unparkSuccessor唤醒后继节点所封装的线程。

public final boolean release(int arg) {
// 尝试释放锁
if (tryRelease(arg)) {
Node h = head;
// 如果头节点不为空,并且waitStatus不为0则唤醒后继节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
// 无论是否执行唤醒后继节点,总会返回true
return true;
}
// 释放失败
return false;
}

接下来就开始分析tryRelease 与unparkSuccessor这两个主要的方法。

尝试释放锁tryRelease(int arg)

tryRelease方法在AQS是默认不实现具体逻辑的,如下:

protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}

因此,我们就拿ReentrantLock的tryRelease的具体实现加以说明,

protected final boolean tryRelease(int releases) {
// 释放后的锁的计数(可重入锁)
int c = getState() - releases;
// 当前释放锁的必须为锁持有的线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 当锁计数为0时说明已锁已完全释放,将AQS中占有线程设为空
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}

唤醒后继节点unparkSuccessor

 private void unparkSuccessor(Node node) {// 唤醒后继节点
int ws = node.waitStatus;
// waitStatus,直接将waitStatus设为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// waitStatus > 0 ,说明该节点已被取消,从后往前遍历找到未被取消距离该节点最近的节点并唤醒
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 唤醒线程
LockSupport.unpark(s.thread);
}

unparkSuccessor的方法执行逻辑:

1.如果头节点waitStatus < 0,就直接将waitStatus 设为0

2.从后往前遍历,找出waitStatus <=0 的节点,并且是离头节点最近的节点,也就是头节点的后继节点

3.找到待唤醒的后继节点后唤醒该节点对应的线程。

以上就是本节要讲的主要内容了,下次再会.....

等等,等等...

以为AQS独占锁的释放过程就此结束了吗?没那么简单。

重新往前看下锁释放的代码,不知道有没有发现问题?

问题

1.tryRelease方法为什么不用CAS进行减少锁计数

2.unparkSuccessor方法中为什么只判断头节点waitStatus<0时,将waitStatus设为0,那么waitStatus>0的情况怎么不判断

3.unparkSuccessor中if (s == null || s.waitStatus > 0) {... },为什么需要判断waitStatus >0

4.为什么需要从后往前遍历找到离头节点最近的并且waitStatus<=0的后继节点进行线程唤醒,不可以从前往后遍历吗?

接下来我们逐一的对以上上个问题进行解释。

问题1 tryRelease方法为什么不用CAS进行减少锁计数

这个问题其实是最简单的一个问题,就是前面也提到的,能够执行到release方法这里来的线程都是已经获取到锁的线程,并且独占锁也只能是一个线程,因此不需要进行CAS进行比较后才赋值。

问题2 unparkSuccessor方法中为什么只判断头节点waitStatus<0时,将waitStatus设为0,那么waitStatus>0的情况怎么不判断

不知道大家还记不记得上一篇分析的内容,重新回顾一下shouldParkAfterFailedAcquire方法,当前驱节点的waitStatus>0时,我们会遍历剔除掉waitStatus>0的节点,因此,当前头节点waitStatus一定不会大于0

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 如果ws == Node.SIGNAL,则说明当前线程已经准备好被唤醒,因此现在可以被阻塞,之后等待被唤醒
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
// 如果ws > 0,说明当前节点已经被取消,因此循环剔除ws>0的前驱节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果ws<=0,则将标志位设置为Node.SIGNAL,当还不可被阻塞,需要的等待下次执行shouldParkAfterFailedAcquire判断是否需要阻塞
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}

问题3 unparkSuccessor中if (s == null || s.waitStatus > 0) {... },为什么需要判断waitStatus >0

在执行过程中,头节点的第一个后继节点的waitStatus >0 时就是节点是被取消的,有可能时因为获取锁超时被取消,因此我们需要跳过该节点的,所以需要重新找下个需要被唤醒的节点,而如果头节点的第一个后继节点的waitStatus<=0直接唤醒。

问题4 为什么需要从后往前遍历找到离头节点最近的并且waitStatus<=0的后继节点进行线程唤醒,不可以从前往后遍历吗?

这个问题我们需要重新回顾上一篇的一个方法addWaiter 跟 enq方法

addWaiter 方法
private Node addWaiter(Node mode) {// 首先尝试快速添加到队尾,失败再正常执行添加到队尾
Node node = new Node(Thread.currentThread(), mode);
// 快速方式尝试直接添加到队尾
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果快速添加到队尾失败则执行enq(node)添加到队尾
enq(node);
return node;
}
enq方法
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 添加到队列尾部
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

从这两个方法可以取出添加节点到同步节点队尾的关键部分进行分析

node.prev = pred; // 步骤1
if (compareAndSetTail(pred, node)) {
pred.next = node; // 步骤2
return node;
}

从上面代码可以看出在执行插入节点的过程中,总是先执行node.prev = pred,然后再执行pred.next = node,因此关于问题4的答案就可以解释了:

如果我们从头往后遍历的话,再并发的环境下如果添加新节点的话可能node.prev = pre已经执行了,但pred.next=node 还未执行,但此时也已经开始执行了unparkSuccessor方法,所以会导致新添加的节点可能没被遍历到,但如果是从后往前遍历的话就不会有该问题。

以上就是AQS独占锁的释放过程,如果有什么问题,欢迎各位不吝指正。

[Java并发] AQS抽象队列同步器源码解析--独占锁释放过程的更多相关文章

  1. [Java并发] AQS抽象队列同步器源码解析--锁获取过程

    要深入了解java并发知识,AbstractQueuedSynchronizer(AQS)是必须要拿出来深入学习的,AQS可以说是贯穿了整个JUC并发包,例如ReentrantLock,CountDo ...

  2. Java并发编程之CAS二源码追根溯源

    Java并发编程之CAS二源码追根溯源 在上一篇文章中,我们知道了什么是CAS以及CAS的执行流程,在本篇文章中,我们将跟着源码一步一步的查看CAS最底层实现原理. 本篇是<凯哥(凯哥Java: ...

  3. 深入理解Java AIO(二)—— AIO源码解析

    深入理解Java AIO(二)—— AIO源码解析 这篇只是个占位符,占个位置,之后再详细写(这个之后可能是永远) 所以这里只简单说一下我看了个大概的实现原理,具体的等我之后更新(可能不会更新了) 当 ...

  4. Java并发包源码学习系列:基于CAS非阻塞并发队列ConcurrentLinkedQueue源码解析

    目录 非阻塞并发队列ConcurrentLinkedQueue概述 结构组成 基本不变式 head的不变式与可变式 tail的不变式与可变式 offer操作 源码解析 图解offer操作 JDK1.6 ...

  5. Java并发编程笔记之AbstractQueuedSynchronizer源码分析

    为什么要说AbstractQueuedSynchronizer呢? 因为AbstractQueuedSynchronizer是JUC并发包中锁的底层支持,AbstractQueuedSynchroni ...

  6. Java并发编程笔记之LinkedBlockingQueue源码探究

    JDK 中基于链表的阻塞队列 LinkedBlockingQueue 原理剖析,LinkedBlockingQueue 内部是如何使用两个独占锁 ReentrantLock 以及对应的条件变量保证多线 ...

  7. AbstractQueuedSynchronizer 队列同步器源码分析

    AbstractQueuedSynchronizer 队列同步器(AQS) 队列同步器 (AQS), 是用来构建锁或其他同步组件的基础框架,它通过使用 int 变量表示同步状态,通过内置的 FIFO ...

  8. Java 集合系列Stack详细介绍(源码解析)和使用示例

    Stack简介 Stack是栈.它的特性是:先进后出(FILO, First In Last Out). java工具包中的Stack是继承于Vector(矢量队列)的,由于Vector是通过数组实现 ...

  9. Fabric1.4源码解析:链码实例化过程

    之前说完了链码的安装过程,接下来说一下链码的实例化过程好了,再然后是链码的调用过程.其实这几个过程内容已经很相似了,都是涉及到Proposal,不过整体流程还是要说一下的. 同样,切入点仍然是fabr ...

随机推荐

  1. windows下大数据开发环境搭建(1)——Hadoop环境搭建

    所需环境 jdk 8 Hadoop下载 http://hadoop.apache.org/releases.html 配置环境变量 HADOOP_HOME: C:\hadoop-2.7.7 Path: ...

  2. 京东物流出问题了?褥了30块羊毛 & 浅析系统架构

    本人亲身经历,但后续的流程分析都是个人猜测的,毕竟没有实际做过这块的业务. 订单物流阻塞经过 火热的双11刚刚退去,截止今日,我在京东购买的矿泉水终于到货啦,下单两箱还只收到了一箱 :( ,从下单到收 ...

  3. SCAU1143 多少个Fibonacci数--大菲波数【杭电-HDOJ-1715】--高精度加法--Fibonacci数---大数比较

    /*******对读者说(哈哈如果有人看的话23333)哈哈大杰是华农的19级软件工程新手,才疏学浅但是秉着校科联的那句“主动才会有故事”还是大胆的做了一下建一个卑微博客的尝试,想法自己之后学到东西都 ...

  4. ArcGIS 重新创建几何服务(GeometryService)

    #参考官方网址:http://enterprise.arcgis.com/zh-cn/server/10.4/administer/windows/re-creating-the-geometry-s ...

  5. PowerMock学习(七)之Mock Constructor的使用

    前言 我们在编码的时候,总习惯在构造器中传参数,那么在powermock中是怎么模拟带参数构造的呢,这并不难. 模拟场景 我们先模拟这样一个场景,通过dao中的传入一个是布尔类型(是否加载)和一个枚举 ...

  6. 【集训Day4 动态规划】轮船问题

    轮船问题(ship) [问题描述] 某国家被一条河划分为南北两部分,在南岸和北岸总共有N对城市,每一城市在对岸都有唯一的友好城市,任何两个城市都没有相同的友好城市.每一对友好城市都希望有一条航线来往, ...

  7. 4.Netty执行IO事件和非IO任务

    回顾NioEventLoop的run方法流程 IO事件与非IO任务 处理IO事件 处理非IO任务 聚合定时任务到普通任务队列 从普通队列中获取任务 计算任务执行的超时时间 安全执行 计算是否超时 总结 ...

  8. 机器学习实战书-第二章K-近邻算法笔记

    本章介绍第一个机器学习算法:A-近邻算法,它非常有效而且易于掌握.首先,我们将探讨女-近邻算法的基本理论,以及如何使用距离测量的方法分类物品:其次我们将使用?7««^从文本文件中导人并解析数据: 再次 ...

  9. Spring面试题集锦(精选)

    以下来自网络收集,找不到原文出处.此次主要为了面试收集,希望对大家有所帮助~~~~ 1.什么是Spring? Spring是一个开源的Java EE开发框架.Spring框架的核心功能可以应用在任何J ...

  10. JS获取当前完整的url地址以及参数的方法

    javascript 获取当前 URL 参数的两种方法: //返回的是字符串形式的参数,例如:class_id=3&id=2& function getUrlArgStr(){ var ...