并发基本原理(四):AQS源码解析1-ReentrantLock的lock实现原理

news2024/9/23 13:22:38

简介

AQS(AbstractQueuedSynchronizer)是JUC包中的核心抽象类,许多并发工具的实现,包括ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore 等都是基于此实现。

AQS,主要提供了表示同步状态的 state 变量,以及抢不到锁时的线程进入的 等待队列 及其排队逻辑。队列使用内部类 Node 进行实现。Node中包含了实现双向队列的必要属性:prev 指向前驱节点、next 指向后继节点,thread 属性指向当前等待节点对应的线程,waitStatus 表示当前线程节点是否正常(节点有可能被取消或其他状态)

AQS 继承自另一个抽象类:AbstractOwnableSynchronizer,AbstractOwnableSynchronizer比较简单,内部维护了 exclusiveOwnerThread 变量表征当前持有锁(独占锁)的线程,对应的getter、setter方法因为只有在获取到锁后会被调用,因此是线程安全的,不需要不同访问。

下面以 ReentrantLock 的实现为例,介绍对应的AQS的继承类与它的配合逻辑。

ReentrantLock

ReentrantLock 内部定义了 AQS 的继承类 Sync,NonfairSync、FairSync 分别对应了非公平锁、公平锁的实现。我们常用的ReentrantLock类的 lock、unlock 方法会委托给 NonfairSync 或 FairSync 的 lock、release 方法实现。

lock 实现

非公平锁 NonfairSync,直接对state变量执行 CAS 原子性操作,如果成功的话,则设置持有锁的线程为当前线程,否则执行 acquire 方法。而 公平锁 FairSync 则是直接执行 acquire 方法。

/**
* Sync object for non-fair locks
 */
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

/**
 * Sync object for fair locks
 */
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

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

NonfairSync#lock

acquire 方法在 AQS 中 进行定义,我们提到 AQS 实现定义了state 变量,但具体使用,以及是否成功获得锁,则是由子类进行定义,因此在 acquire 内部执行了 子类的 tryAcquire 方法,来让子类决定是否成功获得锁,如果成功则直接退出,否则将当前线程加入到线程等待队列中。

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

我们先来看非公平锁的 tryAcquire–nonfairTryAcquire 实现:首先获得state的实时值,如果为 0 表示没有线程占用,立即去抢,抢成功直接返回true,进而退出 acquire。如果当前线程就是持有锁的线程,则对state变量进行累加赋值,同样返回true,退出acquire。否则返回false,表示有其他线程已经持有锁。

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

FairSync#lock

公平锁的实现:拿到state值后,如果为0,没事没有线程占用锁,然后先判断排队队列中是否有线程等待,如果有,则返回flse,将自己添加到队列尾部(AQS的acquire中实现)。如果队列中没有等待线程,再去执行 CAS state 的动作。后续的逻辑与 非公平锁 一致了。

/**
 * Sync object for fair locks
 */
static final class FairSync extends Sync {
    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

一句话总结,公平与非公平锁的区别在于:线程抢锁之前,是否要关注锁的等待队列中有无节点,非公平锁是不看,来了直接抢,公平锁是要看,人家先来的要先执行,我到后面去等着执行。实践证明,非公平锁的效率更高,是因为有可能持有锁的线程很快会释放锁,这样非公平锁直接获取,减少了去内核中阻塞的线程数量,提高了执行的效率。

AbstractQueuedSynchronizer#acquire

lock方法实现的核心逻辑还是在 AQS 的 acquire 中定义,前面我们说过了线程是否成功获得锁的方法 tryAcquire 在子类中的定义,我们再回过来看如果没有成功获得锁,tryAcquire 返回false的情况。

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

addWaiter

首先会执行 addWaiter 方法,将当前线程添加到等待队列中。

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

主要逻辑如下

  1. 创建当前线程对应的Node节点
  2. 如果队里的 tail 指针不为空,则将新建的node设置为新的tail,然后设置原来的 tail 的 next 指针指向当前线程的node。
  3. 如果tail为空,则进入的enq方法中执行。

这里我们可以看到,在set Tail 的时候,也是用的CAS操作,也就是说,在进入队列的时候,也是有并发情况的。我们可以画出添加新节点之前队列的示意图。
AQS-1
在执行 compareAndSetTail 之前,优先将新节点的prev进行了设置,这一步是对线程结构没有任何影响的,然后在 compareAndSetTail 之后设置了 原来的 tail 指向自己,完成整个步骤。

再来看 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;
            }
        }
    }
}
  1. 忽略for循环,tail不为空的情况下与外层的 addWaiter 方法执行逻辑是一致的,这里for循环保证了 compareAndSetTail 一次不成功的情况下继续执行。
  2. 如果tail为空,说明队列还没有被初始化,此时优先初始化 head 指针,然后将 head 赋值给 tail,再下一轮循环中将入参的 node 添加到 tail 后面。

由此可见 addWaiter 在调用 enq 之前,优先判断 tail 不为空,仅执行一次 compareAndSetTail 操作,是一步优化过程,完全可以忽略,直接进入到enq,因为enq中有更完备的逻辑,外层的代码仅作为优化存在。外层执行一次 compareAndSetTail 失败,还是会进入到 enq中的。并且当队列为空时,也必定会进入到enq中,但大多数情况下可能并没有入队时的竞争,因此这里值得此优化。

acquireQueued

线程进入等待队列后,执行到 acquireQueued 方法。按逻辑推理,进入到等待队列的线程,应该去阻塞了吧,这应该是 acquireQueued 的主要目的,但看一下实现逻辑其实并没有这么简单。

/**
 * Acquires in exclusive uninterruptible mode for thread already in
 * queue. Used by condition wait methods as well as acquire.
 *
 * @param node the node
 * @param arg the acquire argument
 * @return {@code true} if interrupted while waiting
 */
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)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
  1. for循环的作用是,在线程被唤醒后,仍然是在这个方法for循环内部,并且因为非公平锁的存在,所以该方法内部调用 tryAcquire 并不一定能够执行成功,因此这里必须有for循环才可。
  2. 获取到当前线程的前驱节点,如果为head(head为当前持有锁正在执行的线程节点),则去尝试获取锁,因为下一个执行的就是自己,如果成功获取,不必再去阻塞了,直接执行即可。
  3. 如果前驱节点不是 head,则执行 shouldParkAfterFailedAcquire 判断并更新前驱节点的状态,如果返回true,表示当前线程节点的前驱节点是正常的,自己应该去执行 parkAndCheckInterrupt 并阻塞,否则需要重复循环并再次执行 shouldParkAfterFailedAcquire。

shouldParkAfterFailedAcquire 简单来说是想检查一遍前面的线程节点,如果有取消状态的,则直接忽略掉,修改新节点的前驱节点为正常状态的临近节点。开头我们提过,node对象中包含有 waitStatus 属性,这里正是检查了该属性的值,如果为 CANCELLED( = 1) 状态,则直接从链表中删除。方法的返回结果表征当前node节点是否应该去阻塞。具体代码如下:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
   int ws = pred.waitStatus;
   if (ws == Node.SIGNAL)
       /*
        * This node has already set status asking a release
        * to signal it, so it can safely park.
        */
       return true;
   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;
   } 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);
   }
   return false;
}

shouldParkAfterFailedAcquire 返回true,则执行 parkAndCheckInterrupt 线程进入阻塞状态了。在被唤醒之时,则会重新进行循环,去执行 if (p == head && tryAcquire(arg)) 的逻辑。

总结

本文解析了 ReentrantLock 的公平锁、非公平锁的实现源码,分析了非公平与公平锁的区别,并认识了AQS的实现。AQS 的 acquire 正是加锁逻辑的模板方法模式。父类中定义算法的整体逻辑,内部调用子类的局部实现,完成整个的逻辑。

同时我们也认识了state的定义与使用,AQS 只声明了改变量,但没有具体定义要怎么使用,具体使用同样交给子类,我们也看到了 ReentrantLock 中的使用 大于0 表示持有锁,等于0表示没有线程持有锁,进一步,我们也可以认识重入锁的原理,即持有锁的线程去访问另一个同样要求获取同一个锁的代码片段的时候,在持有锁的基础上,继续累加 state 实现。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Kafka - 主题Topic与消费者消息Offset日志记录机制

Kafka Topic 可以根据业务类型&#xff0c;分发到不同的Topic中&#xff0c;对于每一个Topic&#xff0c;下面可以有多个分区(Partition)日志文件: kafka 下的Topic的多个分区&#xff0c;每一个分区实质上就是一个队列&#xff0c;将接收到的消息暂时存储到队列中&#xff0…

CTFHub | 过滤空格

0x00 前言 CTFHub 专注网络安全、信息安全、白帽子技术的在线学习&#xff0c;实训平台。提供优质的赛事及学习服务&#xff0c;拥有完善的题目环境及配套 writeup &#xff0c;降低 CTF 学习入门门槛&#xff0c;快速帮助选手成长&#xff0c;跟随主流比赛潮流。 0x01 题目描述…

[附源码]Python计算机毕业设计SSM京东仓库管理系统(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

[附源码]计算机毕业设计项目管理系统的专家评审模块Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

项目管理逻辑:项目如何算是做完?什么是项目管理中的PPP模式?

目录 1.企业中的两件事 2.ppp模式---项目和运营的交织案例 1.企业中的两件事 一个是项目;一个是运营.项目是阶段性一次性的工作; 运营是持续性重复性的工作:职能部门,财务部, 采购部,人力部门,每个月都要报税, 每个月都要招聘, 每个月都要报销, 每天都要记账,. 注意,不确定性…

JMeter入门教程(14)——场景设计

1.JMeter中场景设计是通过线程组来实现的 如图&#xff1a; 控制面板中各元素介绍&#xff1a; 名称&#xff1a;可以随意设置&#xff0c;最好有业务意义。 注释&#xff1a;可以随意设置&#xff0c;可以为空。 在取样器错误后要执行的动作&#xff1a;其中的某一个请求出错后…

MySQL更新一条已经存在的sql语句是怎么执行的

MySQL更新一条已经存在的sql语句是怎么执行的1. 问题描述2. 分析验证1. 问题描述 今天看到一个有意思的问题&#xff0c;就是Mysql更新一条已经存在的语句是怎么执行的&#xff0c;结果显示&#xff0c;匹配(rows matched)了一行&#xff0c;修改(Changed)了0行。&#xff0c;…

sql集锦

sql集锦查询本月数据新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的Ka…

《MongoDB》Mongo Shell中的基本操作-更新操作一览

前端博主&#xff0c;热衷各种前端向的骚操作&#xff0c;经常想到哪就写到哪&#xff0c;如果有感兴趣的技术和前端效果可以留言&#xff5e;博主看到后会去代替大家踩坑的&#xff5e; 主页: oliver尹的主页 格言: 跌倒了爬起来就好&#xff5e; 来个关注吧&#xff0c;点个赞…

“剧情+综艺” 助推国潮文化破圈

一舞千年&#xff0c;重现大唐辉煌&#xff1b;一曲流光&#xff0c;雕琢岁月模样&#xff1b;一纸云烟&#xff0c;漫卷诗书山河&#xff1b;跨历史长河&#xff0c;览盛世华章。自从河南卫视开启“剧情综艺”的晚会形式&#xff0c;晚会便多了一种呈现方式。 从2021年《唐宫夜…

本周XR新闻:吴德周成立AR硬件公司,SideQuest支持PICO、Magic Leap

本周AR/VR大新闻&#xff0c;AR方面&#xff1a;吴德周成立AR硬件公司“致敬未知科技”&#xff1b;彭博称苹果AR操作系统或命名“xrOS”&#xff1b;AR眼镜开源方案OpenAR亮相&#xff1b;Epic 3D扫描工具RealityScan上线&#xff1b;Qoncept推出基于AI的实时姿态追踪系统。 …

Docker_实用篇_Docker-Compose_微服务部署

Docker_实用篇_Docker-Compose_微服务部署 文章目录Docker_实用篇_Docker-Compose_微服务部署4.1Docker-Compose4.2.初识DockerCompose4.3.部署微服务集群4.3.1.打包前文件汇总4.3.2.修改微服务配置4.3.3.打包4.3.4.拷贝jar包到部署目录4.3.5.部署4.1Docker-Compose Docker Co…

Vue(第十六课)JSON-SERVE和POSTMAN技术中对数据的增删改查

今天来聊聊axios技术 同样将官网地址放在博客里: 邮递员API平台|免费注册 (postman.com) json-server - npm (npmjs.com) 起步 | A jsxios 中文文档 | Axios 中文网 (axios-http.cn) 了解一下概念: 1 Axios Axios 是一个基于 promise 网络请求库&#xff0c;作用于node.js …

五、伊森商城 前端基础-Vue p24

目录 1、v-on 2、事件修饰符 3、按键修饰符 3.1、组合按钮 4、v-for 5、v-if和v-show 6、v-else 和 v-else-if 6.1、v-if结合v-for来时用 1、v-on v-on 指令用于给页面元素绑定事件。 语法&#xff1a; v-on:事件名"js 片段或函数名"事件绑定可以简写&#xff…

[附源码]计算机毕业设计基于JEE平台springboot技术的订餐系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Dcoker入门,小白也学得懂!

目录 一、Dcoker简介 二、Centos7安装Docker 三、Docker 容器运行原理解析 四、阿里云镜像仓库配置 一、Dcoker简介 1.1简单介绍 Docker 是一个开源的应用容器引擎&#xff0c;基于 Go 语言 并遵从Apache2.0协议开源。 Docker 可以让开发者打包他们的应用以及依赖包到一个…

【Java难点攻克】「海量数据计算系列」如何使用BitMap在海量数据中对相应的进行去重、查找和排序实战

BitMap&#xff08;位图&#xff09;的介绍 BitMap从字面的意思&#xff0c;很多人认为是位图&#xff0c;其实准确的来说&#xff0c;翻译成基于位的映射&#xff0c;其中数据库中有一种索引就叫做位图索引。 在具有性能优化的数据结构中&#xff0c;大家使用最多的就是hash…

LiteOS-M内核

简介 OpenHarmony LiteOS-M内核是面向IoT领域构建的轻量级物联网操作系统内核&#xff0c;具有小体积、低功耗、高性能的特点&#xff0c;其代码结构简单&#xff0c;主要包括内核最小功能集、内核抽象层、可选组件以及工程目录等&#xff0c;分为硬件相关层以及硬件无关层&…

[附源码]计算机毕业设计校园快递柜存取件系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

多线程复习——进程线程(上)

目录 一、进程是啥?跑起来的程序 二、进程是怎么管理的?描述组织 三、进程的PCB里有啥? 四、进程的调度是咋进行?时间管理大师 五、进程的独立性是咋回事? 六、进程之间如何通信? 一、进程是啥?跑起来的程序 进程(process) 也叫做 任务(task).对于操作系统来说 一个…