JDK17 AQS源码分析

AQS

  • 概览
    • AQS官方解释
    • 简单来说
  • JDK17 中 AQS源码分析
    • Lock 阶段
    • UnLock 阶段
      • 什么时候取消排队呢?

在学习阳哥的 JUC课程的时候,阳哥讲AQS用的是JDK8,我用的是JDK17,想着自己分析一下,分析完之后发现JDK17与JDK8还是有些不同的,感觉更高效了。

概览

AQS全称AbstractQueuedSynchronizer,为实现依赖先进先出(FIFO)等待队列的阻塞锁相关同步器(信号量、事件等)提供了一个框架。

AQS在JUCL包下,如图:

在这里插入图片描述

AQS官方解释

该类被设计为大多数类型的同步器的抽象父类,这些同步器依赖于单个原子int值来表示状态。子类必须定义 改变这种状态 的受保护的方法,这些方法定义了该对象被获取或释放时该状态的含义。考虑到这些,这个类中的其他方法执行所有排队和阻塞机制。子类可以维护其他状态字段,但是只有使getState、setState、compareAndSetState方法操纵的自动更新的int值才会跟踪同步。

子类应该定义为非公共的内部助手类(通常定义成了Sync),用于实现其外围类的同步属性。类AbstractQueuedSynchronizer不实现任何同步接口。相反,它定义了诸如acquireInterruptibly之类的方法,具体 锁和相关同步器 可以在适当的时候调用这些方法来实现它们的公共方法。

该类支持默认独占模式共享模式。当以独占模式获取时,其他线程的尝试获取无法成功。多线程获取共享模式可能(但不需要)成功。这个类不“理解”这些差异,除非从机械意义上说,当共享模式获取成功时,下一个等待线程也必须确定它是否也可以获取。在不同模式下等待的线程共享同一个FIFO队列。通常,实现的子类只支持这些模式中的一种,但这两种模式都可以发挥作用,例如在ReadWriteLock中。仅支持独占模式或仅支持共享模式的子类不需要定义支持未使用模式的方法。

简单来说

就是AQS作为一个模板父类,提供了一个大致的办事流程,具体的细节由子类实现。比如说一个锁,如果有线程使用,当前的state就是1,表示使用中,如果又有线程使用,那么进入AQS的FIFO双端队列当中,FIFO双端队列保存的是Node节点,每个Node节点有下述属性:

volatile Node prev;       
volatile Node next;       
Thread waiter; // 等待线程           
volatile int status;  // 当前线程的等待状态    

线程的等待状态

static final int WAITING   = 1;          //  等待状态
static final int CANCELLED = 0x80000000; //  不等待了
static final int COND      = 2;          // in a condition wait // 不太清楚

JDK17 中 AQS源码分析

ReentrantLock 非公平锁 为例

ReentrantLock reentrantLock = new ReentrantLock(false);


try {
    reentrantLock.lock();
} finally {
    reentrantLock.unlock();
}

假设有三个线程A,B,C依次获取锁

Lock 阶段

难懂的函数:

  • setPrevRelaxed(t):将调用者的pre设置成节点t。
  • tryInitializeHead:初始化dummy节点,当tail为空的时候调用,调用之后head、tail都指向dummy节点。
  • casTail(ex, now):通过cas的方式将tail节点指向now节点。
  • initialTryLock():先抢占一下锁,如果没有人用或者是可以重入,返回true,否则返回false

对于线程A来说

在lock的时候会调用helper的lock方法

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

在helper中的lock方法

final void lock() {
    if (!initialTryLock())
        acquire(1);
}
abstract boolean initialTryLock();

非公平锁的initialTryLock()方法

final boolean initialTryLock() {
    Thread current = Thread.currentThread();
    // 尝试获取锁,如果没有人拥有锁,当前线程拥有锁
    if (compareAndSetState(0, 1)) { // first attempt is unguarded
        // 将当前线程设置为锁的拥有者
        setExclusiveOwnerThread(current);
        return true;
    } else if (getExclusiveOwnerThread() == current) { // 是否是自己,因为可能是可重入的
        int c = getState() + 1; // 锁冲入次数 + 1
        if (c < 0) // overflow // 重入次数太多,溢出了
            throw new Error("Maximum lock count exceeded");
        setState(c);
        return true;
    } else // 已经有人用了,返回false
        return false;
}

经过源码分析,可以根据initialTryLock的返回值得出结论:

  • false:已经有线程获取锁了,排队等候取
  • True:自己拥有锁,或者重入锁

线程A是第一个lock的线程,因此方法返回true,AQS的 state = 1。


对于线程B来说,initialTryLock方法返回false,进入acquire(1)方法,排队获取锁

/**
以独占模式获取,忽略中断。通过调用至少一次tryAcquire实现,成功时返回。
否则,线程将被排队,可能会反复阻塞和解除阻塞,调用tryAcquire直到成功。
*/
public final void acquire(int arg) {
    // 尝试获取锁
    if (!tryAcquire(arg))
        // 排队获取锁
        acquire(null, arg, false, false, false, 0L);
}

这里调用了tryAcquire方法,看一下非公平锁的实现方法

protected final boolean tryAcquire(int acquires) {
    // 第一个 getState()获取当前锁的状态,已经被获取了,返回false,没有的话是true
    
    if (getState() == 0 && compareAndSetState(0, acquires)) {
        // 没人用,抢占锁
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    }
    return false;
}

由于A已经获得锁,因此state >= 1,所以这次返回了false, 进入acquire(null, arg, false, false, false, 0L);方法

// 主获取方法,由所有导出的获取方法调用。
/**
    param:
        node: 除非有重新获取条件,否则是null
        arg: 获取参数,1表示要抢占
        shared: 如果是共享模式则为true
        interruptible: 如果true,中断的时候返回负数,否则中断当前线程
        timed: 如果为真,则使用定时等待
        time: 如果timed为true,这里表示定时多久后抢占
    return:
        如果获取了返回正值,如果超时了返回0,如果被中断了,返回负数
*/
final int acquire(Node node, int arg, boolean shared,
                  boolean interruptible, boolean timed, long time) {
    // 获取当前线程
    Thread current = Thread.currentThread();
    // 取消第一个线程的停放后重试
    byte spins = 0, postSpins = 0;   // retries upon unpark of first thread
    // 是否中断?
    boolean interrupted = false, first = false;
    Node pred = null;                // predecessor of node when enqueued

    /*
     * Repeatedly:
     *  Check if node now first
     *    if so, ensure head stable, else ensure valid predecessor
     *  if node is first or not yet enqueued, try acquiring
     *  else if node not yet created, create it
     *  else if not yet enqueued, try once to enqueue
     *  else if woken from park, retry (up to postSpins times)
     *  else if WAITING status not set, set and retry
     *  else park and clear WAITING status, and check cancellation
     */

    for (;;) {
        /**
            满足:
                1. 不是第一个节点
                2. pred表示当前节点的前一个节点,前一个节点不是Null
                3. 不是第一个节点
        */
        if (!first && (pred = (node == null) ? null : node.prev) != null &&
            !(first = (head == pred))) {
            if (pred.status < 0) {
                cleanQueue();           // predecessor cancelled
                continue;
            } else if (pred.prev == null) {
                Thread.onSpinWait();    // ensure serialization
                continue;
            }
        }
        /**
            满足: 是第一个节点或者前一个是null(没有入队呢)
        */
        if (first || pred == null) {
            // 是否获得锁
            boolean acquired;
            try {
                if (shared)
                    acquired = (tryAcquireShared(arg) >= 0);
                else
                    acquired = tryAcquire(arg); // 独占模式,再次尝试获取锁
            } catch (Throwable ex) {
                cancelAcquire(node, interrupted, false);
                throw ex;
            }
            if (acquired) {
                if (first) {
                    node.prev = null;
                    head = node;
                    pred.next = null;
                    node.waiter = null;
                    if (shared)
                        signalNextIfShared(node);
                    if (interrupted)
                        current.interrupt();
                }
                return 1;
            }
        }
        // 如果当前node是空node,创建node
        if (node == null) {                 // allocate; retry before enqueue
            if (shared)
                node = new SharedNode();
            else
                node = new ExclusiveNode();
        } else if (pred == null) {          // try to enqueue
            node.waiter = current;
            Node t = tail;
            node.setPrevRelaxed(t);         // avoid unnecessary fence
            if (t == null)
                tryInitializeHead();
            else if (!casTail(t, node))
                node.setPrevRelaxed(null);  // back out
            else
                t.next = node;
        } else if (first && spins != 0) {
            --spins;                        // reduce unfairness on rewaits
            Thread.onSpinWait();
        } else if (node.status == 0) {
            node.status = WAITING;          // enable signal and recheck
        } else {
            long nanos;
            spins = postSpins = (byte)((postSpins << 1) | 1);
            if (!timed)
                LockSupport.park(this);
            else if ((nanos = time - System.nanoTime()) > 0L)
                LockSupport.parkNanos(this, nanos);
            else
                break;
            node.clearStatus();
            if ((interrupted |= Thread.interrupted()) && interruptible)
                break;
        }
    }
    return cancelAcquire(node, interrupted, interruptible);
}

对于线程B,B不是FIFO队列的第一个节点,并且node是空,会先创建第一个node节点。
然后再次尝试获取锁,如果没有的话初始化当前节点,waiter设置为当前线程
如果FIFO队列的tail节点是空的,说明此时队列为空,调用tryInitializeHead方法创建一个dummy node。如下图所示:
在这里插入图片描述

然后再次循环到这里,Node t = tail;node.setPrevRelaxed(t);之后,当前节点的pre指向了tail节点,然后调用casTail方法,将当前B线程所在的node设置成tail,如果成功,将上一次的tail的next指向当前node.

在这里插入图片描述

再次回到循环开始的地方,当前是第一个节点,不进入第一个循环,再次尝试获取锁,这里假设还没有获取锁,将node的状态设置成WAITING即1表示进入等待状态,只等资源释放了。
然后再次循环到spins = postSpins = (byte)((postSpins << 1) | 1);if (!timed)LockSupport.park(this);,将自己挂起,等待唤醒。
此时 spins = 1((postSpins << 1) | 1 等价于 2 * postSpins + 1)


当线程C尝试获取锁的时候,进入acquire方法 当前节点是null,创建一个node节点,将waiter设置为当前线程。然后获取tail节点,也就是B所在的node,将当前节点指向tail。

在这里插入图片描述
然后将当前节点设置成tail节点,上一次的tail节点(也就是B)指向当前节点

在这里插入图片描述
然后回到循环的头部,第一个if条件满足了,也就是下面的满足了,但是里面的又都不满足,因此什么也不做.

if (!first && (pred = (node == null) ? null : node.prev) != null &&
    !(first = (head == pred))) {
    if (pred.status < 0) {
        cleanQueue();           // predecessor cancelled
        continue;
    } else if (pred.prev == null) {
        Thread.onSpinWait();    // ensure serialization
        continue;
    }
}

直接到node.status = WAITING;将当前的status也设置成1,之后进入最后一个else,park挂起一下,等待唤醒。

这就是lock的过程。

这里每次都要tryAcquire是因为非公平的模式。

UnLock 阶段

难懂的方法

  • getAndUnsetStatus(status):将调用者的status字段设置成0

unlock会调用helper的release方法:


public void unlock() {
    sync.release(1);
}
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        signalNext(head);
        return true;
    }
    return false;
}

首先尝试释放,如果是最后一次重入,那么执行signalNext(head);方法

tryRelease 方法

@ReservedStackAccess
protected final boolean tryRelease(int releases) {
    int c = getState() - releases; // 当前状态减去依次,可能重入,所以此时c可能依然大于0
    if (getExclusiveOwnerThread() != Thread.currentThread()) // 无意外都是true
        throw new IllegalMonitorStateException();
    boolean free = (c == 0); // 重入的线程是否都出去了
    if (free) // 如果是,让出锁来
        setExclusiveOwnerThread(null);
    setState(c); // 更新重入次数
    return free; // 返回释放结果
}

signalNext 方法

private static void signalNext(Node h) {
    // 后继节点
    Node s;
    // 当前 FIFO队列不空,并且不是初始化状态
    if (h != null && (s = h.next) != null && s.status != 0) {
        s.getAndUnsetStatus(WAITING);
        LockSupport.unpark(s.waiter);
    }
}

这个时候满足条件,会进入if代码块,将当前状态设置为0

final int getAndUnsetStatus(int v) {     // for signalling
    return U.getAndBitwiseAndInt(this, STATUS, ~v);
}
// ~ 按位取反

然后将当前线程unpark
此时,在acquire挂起的线程继续执行
在这里插入图片描述
然后执行下面的尝试抢占锁


if (first || pred == null) {
    boolean acquired;
    try {
        if (shared)
            acquired = (tryAcquireShared(arg) >= 0);
        else
            acquired = tryAcquire(arg);
    } catch (Throwable ex) {
        cancelAcquire(node, interrupted, false);
        throw ex;
    }
    if (acquired) {
        if (first) {
            node.prev = null;
            head = node;
            pred.next = null;
            node.waiter = null;
            if (shared)
                signalNextIfShared(node);
            if (interrupted)
                current.interrupt();
        }
        return 1;
    }
}

如果抢到了,将当前node内容置空,dummy的next指向null,当前节点的pre指向null。FIFO双链表的头节点指向当前节点,如下:
在这里插入图片描述

然后一步步退回到acquire, lock,方法。继续执行操作共享资源代码。


这只是正常流程。如果排队的CANCELLED , 在后面线程lock的时候,会clean掉它.

if (pred.status < 0) {
    cleanQueue();           // predecessor cancelled
    continue;
}

这里就暂时不看具体逻辑了

什么时候取消排队呢?

if ((interrupted |= Thread.interrupted()) && interruptible)

也就是说如果 出现超时或者线程被打断了 并且 当前是可以被打断的。或者出现异常了 进入下面方法

private int cancelAcquire(Node node, boolean interrupted,
                          boolean interruptible) {
    if (node != null) {
        node.waiter = null;
        node.status = CANCELLED;
        if (node.prev != null) // 如果当前不是第一个,调用一次cleanQueue清空一下队列中状态是取消的节点
            cleanQueue();
    }
    if (interrupted) {
        if (interruptible)
            return CANCELLED;
        else
            Thread.currentThread().interrupt();
    }
    return 0;
}

to be continue…

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/684963.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux系统之fc命令的基本使用

Linux系统之fc命令的基本使用 一、fc命令介绍1.1 fc命令简介1.2 fc命令用途 二、fc命令的帮助信息2.1 fc的man帮助2.2 fc命令的使用帮助2.3 fc命令与history命令区别 三、fc命令的基本使用3.1 显示最近执行的命令3.2 指定序号查询历史命令3.3 使用vim编辑第n条历史命令3.4 替换…

ElementUI之el-tooltip显示多行内容

ElementUI之el-tooltip显示多行内容 文章目录 ElementUI之el-tooltip显示多行内容1. 多行文本实现2. 实现代码3. 展示效果 1. 多行文本实现 展示多行文本或者是设置文本内容的格式&#xff0c;使用具名 slot 分发content&#xff0c;替代tooltip中的content属性。 2. 实现代码 …

JAVA-学习-2

一、类 1、类的定义 把相似的对象划分了一个类。 类指的就是一种模板&#xff0c;定义了一种特定类型的所有对象的属性和行为 在一个.java的问题件中&#xff0c;可以有多个class&#xff0c;但是智能有一个class是用public的class。被声明的public的class&#xff0c;必须和文…

【CTF-Web】文件上传漏洞学习笔记(ctfshow题目)

文件上传 文章目录 文件上传What is Upload-File&#xff1f;Upload-File In CTFWeb151考点&#xff1a;前端校验解题&#xff1a; Web152考点&#xff1a;后端校验要严密解题&#xff1a; Web153考点&#xff1a;后端校验 配置文件介绍解题&#xff1a; Web154考点&#xff1a…

ChatTTS webUI API:ChatTTS本地网页界面的高效文本转语音、同时支持API调用!

原文链接&#xff1a;&#xff08;更好排版、视频播放、社群交流、最新AI开源项目、AI工具分享都在这个公众号&#xff01;&#xff09; ChatTTS webUI & API&#xff1a;ChatTTS本地网页界面的高效文本转语音、同时支持API调用&#xff01; &#x1f31f;一个简单的本地网…

【Python学习1】matplotlib和pandas库绘制人口数变化曲线

✍&#x1f3fb;记录学习过程中的输出&#xff0c;坚持每天学习一点点~ ❤️希望能给大家提供帮助~欢迎点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb;指点&#x1f64f; 一、Python库说明 Matplotlib Matplotlib是一个功能强大的Python 2D绘图库&#xff0c;它允…

汇编:x86汇编环境搭建与基础框架(32位)

32位汇编代码编写环境&#xff1a;Visual Studio&#xff08;笔者用的版本为2017&#xff09;&#xff1b;先来说一下在Visual Studio 2017中编写汇编代码的准备操作&#xff1a; ①创建空项目 ②设置项目属性&#xff1a;平台工具集设置为Visual Studio 2015(v140)&#xff0…

怎么用PHP语言实现远程控制两路照明开关

怎么用PHP语言实现远程控制两路开关呢&#xff1f; 本文描述了使用PHP语言调用HTTP接口&#xff0c;实现控制两路开关&#xff0c;两路开关可控制两路照明、排风扇等电器。 可选用产品&#xff1a;可根据实际场景需求&#xff0c;选择对应的规格 序号设备名称厂商1智能WiFi墙…

搜索与图论:深度优先搜索

搜索与图论&#xff1a;深度优先搜索 题目描述参考代码 题目描述 参考代码 #include <iostream>using namespace std;const int N 10;int n; int path[N]; bool st[N];void dfs(int u) {// u n 搜索到最后一层if (u n){for (int i 0; i < n; i) printf("%d …

中国游戏产业月度报告分享 | 洞察游戏行业市场

作为中国音像与数字出版协会主管的中国游戏产业研究院的战略合作伙伴&#xff0c;伽马数据发布了《2024年4月中国游戏产业月度报告》。 数据显示&#xff0c; 2024年4月&#xff0c;中国游戏市场实际销售收入224.32亿元&#xff0c;环比下降4.21%&#xff0c;同比下降0.27%。移…

Qt无边框

最简单的可拖动对话框(大小不可改变) #ifndef DIALOG_H #define DIALOG_H/*** file dialog.h* author lpl* brief 无边框dialog类* date 2024/06/05*/ #include <QDialog> #include <QMouseEvent> namespace Ui { class Dialog; } /*** brief The Dialog class* 无…

如何把试卷上的字去掉再打印?分享三种方法

如何把试卷上的字去掉再打印&#xff1f;随着科技的不断发展&#xff0c;现代教育和学习方式也在逐渐变革。在学习过程中&#xff0c;我们经常需要对试卷进行整理和分析&#xff0c;以便更好地掌握知识点和复习。然而&#xff0c;传统的试卷整理方法往往效率低下且容易出错。幸…

六月的魔力:揭秘2024年加密市场与Reflection的创新与收益

回想过去加密货币市场的沉浮&#xff0c;一年中市场的阶段性牛市大多发生在下半年&#xff0c;六月似乎是一个神奇的时间节点。每年六月一到&#xff0c;加密货币市场仿佛突然被按下启动按钮&#xff0c;沉寂的土狗开始扶苏&#xff0c;经过半年准备的各大项目方开始蠢蠢欲动。…

27-unittest之断言(assert)

在测试方法中需要判断结果是pass还是fail&#xff0c;自动化测试脚本里面一般把这种生成测试结果的方法称为断言&#xff08;assert&#xff09;。 使用unittest测试框架时&#xff0c;有很多的断言方法&#xff0c;下面介绍几种常用的断言方法&#xff1a;assertEqual、assert…

MySql每天从0开始生成特定规则自增编号

一、前言 1、按一定规则生单号&#xff0c;要求不重复 2、例如&#xff1a;前缀 日期 不重复流水号&#xff0c;whgz-20240528-00001 二、数据库操作 1、MySQL新建一张表sys_sequence seq_name 序列名称 current_val 当前编号 increment_val 步长 CREATE TABLE sys_sequ…

kafka-消费者-消费异常处理(SpringBoot整合Kafka)

文章目录 1、消费异常处理1.1、application.yml配置1.2、注册异常处理器1.3、消费者使用异常处理器1.4、创建生产者发送消息1.5、创建SpringBoot启动类1.6、屏蔽 kafka debug 日志 logback.xml1.7、引入spring-kafka依赖1.8、消费者控制台&#xff1a;1.8.1、第一次启动SpringK…

【案例分享】明道数云为阿联酋迪拜公司Eastman BLDG打造外贸管理系统

内容概要 本文介绍了Eastman公司与明道数云软件的合作&#xff0c;通过数字化解决方案提升了Eastman在贸易管理中的效率。Eastman公司位于阿联酋迪拜&#xff0c;周边城市有门店&#xff0c;人数大概在30&#xff0c;是一家主营瓷砖和石材类产品的贸易公司&#xff0c;面临着各…

C#,JavaScript实现浮点数格式化自动保留合适的小数位数

目标 由于浮点数有漂移问题&#xff0c;转成字符串时 3.6 有可能得到 3.6000000000001&#xff0c;总之很长的一串&#xff0c;通常需要截取&#xff0c;但按照固定长度截取不一定能使用各种情况&#xff0c;如果能根据数值大小保留有效位数就好了。 C#实现 我们可以在基础库里…

linux实验报告

实验一&#xff1a;Linux操作系统的安装与配置 实验目的&#xff1a; 1.掌握虚拟机技术&#xff1b; 2.掌握Linux的安装步骤&#xff1b; 3.掌握安装过程中的基本配置要求。 4.掌握正确启动Linux的方法&#xff1b; 5.掌握正确退出Linux的方法&#xff1b; 6.熟悉已安装…

【Python】把xmind转换为指定格式txt文本

人工智能训练通常需要使用文本格式&#xff0c;xmind作为一种常规格式不好进行解析&#xff0c;那如何把xmind转换为txt格式呢&#xff1f; 软件信息 python python -v Python 3.9.13 (tags/v3.9.13:6de2ca5, May 17 2022, 16:36:42) [MSC v.1929 64 bit (AMD64)] on win32…