【并发编程】AQS源码

news2025/1/15 15:13:22

ReentrantLock

互斥锁,可重入
AQS是可以支持互斥锁和共享锁的,这里只分析互斥锁的源码

加锁

公平锁和非公平锁

  • 公平锁
	final void lock() {
		acquire(1); //抢占1把锁.
	}
	
	// AQS里面的方法
	public final void acquire(int arg) { 
		if (!tryAcquire(arg) &&
			acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
			selfInterrupt();
		}
	protected final boolean tryAcquire(int acquires) {
		final Thread current = Thread.currentThread();
		int c = getState();
		if (c == 0) { //表示无锁状态
			if (!hasQueuedPredecessors() &&
				compareAndSetState(0, acquires)) { //CAS原子操作	
				setExclusiveOwnerThread(current); //把获得锁的线程保存到exclusiveOwnerThread中
				return true;
			}
		}
		//如果当前获得锁的线程和当前抢占锁的线程是同一个,表示重入
		else if (current == getExclusiveOwnerThread()) {
		    //增加重入次数.
			int nextc = c + acquires; 
			if (nextc < 0)
				throw new Error("Maximum lock count exceeded");
			setState(nextc); //保存state
			return true;
		}
		return false;
	}
  • 非公平锁
    final void lock() {
        //非公平锁,不管当前AQS队列中是否已有线程在排队的,都先去插队(尝试获得锁)
        if (compareAndSetState(0, 1)) //返回false表示抢占锁失败
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
    //AQS里的方法
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
    }
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            //hasQueuedPredecessors 和公平锁的区别,公平锁是如果已经有在排队的线程了,
            // 那么新过来的线程就不允许插队(去尝试获取锁)
            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;
    }
  • 加入队列并进行自旋等待
public final void acquire(int arg) {
    //如果尝试获取锁失败,则会加入队列并进行自旋等待
	if (!tryAcquire(arg) &&
		acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}

acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

  • addWaiter(Node.EXCLUSIVE) -> 添加一个互斥锁的节点
  • acquireQueued() -> 自旋锁和阻塞的操作
    private Node addWaiter(Node mode) {
        //把当前线程封装成一个Node节点。后续唤醒线程的时候,需要得到被唤醒的线程.
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        //假设不存在竞争,那么一次CAS操作就可以将Node节点加入到双向链表,如果存在竞争,最终还是要通过enq里的自旋来加入到双向链表中
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //第一次添加Waiter的时候pred一定为空,会直接执行enq方法
        enq(node);
        return node;
    }
    //从尾部添加到链表,尾插法
    private Node enq(final Node node) {
        //自旋
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                //初始化一个head节点,注意Head节点是一个空节点,不存储任何线程信息
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //注意这里先设置的新节点node的prev,所以后续在遍历链表的时候都是从tail->head
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

在这里插入图片描述

    //node表示当前来抢占锁的线程节点,可能是Thread B,Thread C
    final boolean acquireQueued(final Node node, int arg) {
        //标记是否成功拿到锁
        boolean failed = true;
        try {
            //标记等待过程中是否中断过
            boolean interrupted = false;
            for (;;) { //自旋
                //begin
                //获取当前节点的前置节点,如果是head(则表示这个线程是排在第一个),会尝试去获取锁
                //tryAcquire 公平锁里判断当前节点是否有前置节点来决定能否去抢占锁
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    //拿到资源后,将head指向该结点。
                    setHead(node);
                    //setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。
                    p.next = null;
                    failed = false;
                    //返回等待过程中是否被中断过
                    return interrupted;
                }
                //end begin->end 这里会尝试去获得锁,如果失败的话会让线程去阻塞(park),如果设置前驱节点节点的signal状态失败,那么会再次尝试去获取锁,失败后再次尝试设置(自旋)
                if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt()) //LockSupport.park
                    //如果等待过程中被中断过,就将interrupted标记为true,注意此时的failed=true,因为是通过中断唤醒的,并没有获取到锁资源
                    interrupted = true;
            }
        } finally {
            //如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
            if (failed)
                cancelAcquire(node);
        }
    }

先来看下shouldParkAfterFailedAcquireparkAndCheckInterrupt()的实现再来看acquireQueued的代码,Node节点的状态有如下几种:

  • SIGNAL: -1 表示它的下一个节点处于park状态,如果当前节点取消或者释放锁资源的时候需要unpark下一个节点
  • CANCELLED: 1 :当前节点由于超时或者被中断而处于取消状态
  • CONDITION:-2 共享锁使用的状态
  • PROPAGATE: -3
    默认为0
 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            //前驱节点已经是SIGNAL状态了,当前节点可以安心去park
            return true;
        if (ws > 0) {
            //如果前驱节点处于CANCELLED状态,那么就需要遍历链表(tail->head),直到找到最近一个正常状态的节点,排在其后
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //如果前驱节点非CANCELLED状态,那就把前驱节点的状态设置成SIGNAL(告诉他等他释放锁资源需要通知自己) 并发场景下CAS可能会失败(失败后会在外层方法自旋,再次触发该方法)
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

这里之所以通过 node.prev = pred = pred.prev; 从后往前遍历,去掉为CANCELLED状态的节点,因为我们在设置双向链表的时候是先设置的tail的prev节点,如果从前往后操作,并发场景下可能会出现next指针未指向的情形,导致异常

在整个过程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去park,需要去找个安心的休息点,同时可以再尝试下看有没有机会获取锁资源。

    //ThreadB、 ThreadC -> 都会阻塞在下面这个代码的位置.
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this); //调用park()使线程进入waiting状态
        return Thread.interrupted(); //如果被唤醒,查看自己是不是被中断唤醒的。线程除了被正常唤醒之外,interrupt方法也会唤醒线程 Thread.interrupted()判断当前线程是否被中断过
    }
 

再来总结下acquireQueued()的流程:
node进入队尾后
1.先判断前驱节点是否为head,如果是则尝试去获取锁资源,拿到锁资源后就将head指向当前节点
2. 获取不到锁资源则检查前驱节点的状态,找到安全休息点(前驱节点为SIGNAL状态)则调用park进入等待状态
3.被唤醒后,先判断从入队到唤醒过程中是否有被中断过,如果有的话则将本节点置为CANCELLED状态,否则尝试去获取锁资源,执行步骤1。

解锁

   public final boolean release(int arg) {
        if (tryRelease(arg)) {
            //得到当前AQS队列中的head节点
            Node h = head; 
            //head节点不为空且状态不是0,说明这个节点不是链表里的最后一个节点
            if (h != null && h.waitStatus != 0) 
                //唤醒下一个节点(在里面会调用unpark方法)
                unparkSuccessor(h); 
            return true;
        }
        return false;
    }

    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0) //SIGNAL状态,表示为可以唤醒状态
            compareAndSetWaitStatus(node, ws, 0); //恢复成0
        //查找下一个需要唤醒的结点s
        Node s = node.next;
        //说明ThreadB这个线程可能已经被销毁,或者出现异常...
        if (s == null || s.waitStatus > 0) {
            s = null;
            //从tail -> head进行遍历,进行节点的移除
            for (Node t = tail; t != null && t != node; t = t.prev)
                //查找到小于等于0的节点(我们之前修改节点的状态是将前驱节点设置为SIGNAL,所以最后一个节点的状态是默认的0)
                if (t.waitStatus <= 0) 
                    s = t;
        }
        if (s != null)
            //唤醒封装在Node中的被阻塞的线程)
            LockSupport.unpark(s.thread); 
       
    }
    
     protected final boolean tryRelease(int releases) {
          int c = getState() - releases;
          if (Thread.currentThread() != getExclusiveOwnerThread())
              throw new IllegalMonitorStateException();
          boolean free = false;
          if (c == 0) {
              free = true;
              setExclusiveOwnerThread(null);
          }
          setState(c);
          return free;
     }

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

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

相关文章

MySQL:事务、索引、用户管理、备份、数据库设计(三大范式)

文章目录Day 03&#xff1a;一、事务1. 原则2. 测试实现二、索引1. 分类2. 创建索引3. 分析 sql 执行的状况4. 测试索引5. 索引原则三、数据库用户管理四、备份五、规范数据库设计1. 三大范式注意&#xff1a;Day 03&#xff1a; 一、事务 事务(transaction)&#xff1a;要么…

含光热电站、有机有机朗肯循环、P2G的综合能源优化调度(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

『pyqt5 从0基础开始项目实战』04. 表格数据的初始化(保姆级图文)

目录导包和框架代码准备json数据文件表格数据接入1. 准备文件路径2. 读取json数据3. 将得到的json数据放入table设置单元格不可修改把数据中的数字转为映射内容完整代码总结欢迎关注 『pyqt5 从0基础开始项目实战』 专栏&#xff0c;持续更新中 欢迎关注 『pyqt5 从0基础开始项…

TiDB实战篇-备份恢复策略

简介 简要说明TiDB备份恢复策略。 备份的类型 热备 TiDB使用MVCC机制实现设备的。 冷备 需要停机备份。 温备 备份的时候只能读不能够写。 备份技术 逻辑备份 物理备份 物理备份的限制 基于复制的备份 复制恢复是最快的。&#xff08;TiDB CDC,TiDB Binlog&#xff…

【C语言】函数详解(嵌套调用和链式访问、声明及定义、递归)

简单不先于复杂&#xff0c;而是在复杂之后。 目录 1.函数的嵌套调用和链式访问 1.1 嵌套调用 1.2 链式访问 2. 函数的声明和定义 2.1 函数声明 2.2 函数定义 3. 函数递归 3.1 什么是递归&#xff1f; 3.2 递归的两个必要条件 3.2.1 练习1&#xff08;需要画图…

Spring Security实战(三)—— 自动登录与注销登录

目录 一、实现自动登录 1. 散列加密方案 2. 持久化令牌方案 二、注销登录 一、实现自动登录 自动登录是将用户的登录信息保存在用户浏览器的cookie中&#xff0c;当用户下次访问时&#xff0c;自动实现校验并建立登录态的一种机制。 Spring Security 提供了两种非常好的令牌&a…

C ++ 基础入门。加强变量、指针、结构体理解

1、 const放外面&#xff0c;值不可以改。只读 同理于指针 看const右侧紧跟着的是指针还是常量, 是指针就是常量指针&#xff0c;是常量就是指针常量 const 放外面&#xff0c;值不可以改 2、 所有的指针类型&#xff0c;包括结构体指针 double * int *都是和操作系统位数…

补充——spark RDD序列化和持久化

目录 RDD序列化 闭包检查&#xff1a; 序列化方法和属性 Kryo序列化框架&#xff1a; RDD持久化&#xff08;RDD persistence&#xff09; RDDCache缓存 RDD persist缓存 什么时候使用persist()? RDD CheckPoint 检查点 缓存和检查点区别 RDD序列化 闭包检查&#x…

JavaScript 的基础函数有哪些?

1、在 JavaScript 中将数组本地转换为对象 JavaScript 有一个原生函数 Object.fromEntries&#xff0c;可用于将任何输入数组转换为对象。 1.const anArray [ 2. [firstname, Paul], 3. [surname, Knulst], 4. [address, worldwide], 5. [role, Senior Engineer], 6. […

Java中的异常Exception和捕获,自定义异常

文章目录1. 异常概述1.1 什么是程序的异常1.2 异常的抛出机制1.3 如何对待异常2. Java异常体系2.1 Throwable2.2 Error 和 Exception2.3 编译时异常和运行时异常3. 常见的错误和异常3.1 Error3.2 运行时异常3.3 编译时异常4. 异常的处理4.1 异常处理概述4.2 捕获异常&#xff0…

springboot整合websocket

1.创建springboot项目&#xff0c;引入spring-boot-starter-websocket依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency>全部依赖如下&#xff1a; &l…

JDBC之DAO层封装思想超详解

Mysql版本&#xff1a;8.0.26 可视化客户端&#xff1a;sql yog 编译软件&#xff1a;IntelliJ IDEA 2019.2.4 x64 运行环境&#xff1a;win10 家庭中文版 jdk版本&#xff1a;1.8.0_361 目录一、DAO是什么&#xff1f;二、案例演示2.1 准备数据2.2 创建bean包2.3 建立DAO包2.2…

Houdini>RBD(搅拌大米效果)并导出FBX到unity

Houdini&#xff1e;RBD(搅拌大米效果) 效果展示&#xff1a; 动图录制软件&#xff1a;Cockos Incorporated | LICEcap 参考链接&#xff1a;导出除了ABC外&#xff0c;比较小的FBX文件用法 目录&#xff1a; 一、引用模型的处理&#xff1a; 1、大米 模型创建 多层复制 …

Mybatis(六)缓存

缓存是Mybatis中非常重要的特性&#xff0c;Mybatis的一级缓存基于SqlSession实现&#xff0c;二级缓存基于Mapper实现。 一、缓存的使用 一级缓存默认开启&#xff0c;Mybatis提供了一个配置参数localCacheScope来控制一级缓存的级别&#xff0c;该参数的取值可以是session、…

【机器学习】P10 从头到尾实现一个线性回归案例

这里写自定义目录标题&#xff08;1&#xff09;导入数据&#xff08;2&#xff09;画出城市人口与利润图&#xff08;3&#xff09;计算损失值&#xff08;4&#xff09;计算梯度下降&#xff08;5&#xff09;开始训练&#xff08;6&#xff09;画出训练好的模型&#xff08;…

参加Matlab与AI讲座:使用深度强化学习训练走路机器人观后感

时间&#xff1a;2023年4月12日&#xff0c;周三&#xff0c;天气晴 地址&#xff1a;大连理工大学研教楼303 前言&#xff1a;Matlab其实有很多功能&#xff0c;我们所用的只是最基础最简单的部分&#xff0c;例如矩阵计算&#xff0c;画图等等。 随着强化学习的发展&#xff…

一般形式的S曲线公式推导

文章目录一、背景二、目标三、计算3.1 S曲线基本形式3.2 S曲线变换3.3 参数计算3.4 S曲线中心对称条件四、总结五、附件一、背景 S曲线因具备良好可控的平滑性、单调性、连续可导性等优点&#xff0c;常作为各类电机升降速曲线。当前多数S曲线的介绍文章未给出推导过程&#x…

SpringCloud微服务技术栈.黑马跟学(五)

SpringCloud微服务技术栈.黑马跟学 五今日目标1.初识elasticsearch1.1.了解ES1.1.1.elasticsearch的作用1.1.2.ELK技术栈1.1.3.elasticsearch和lucene1.1.4.为什么不是其他搜索技术&#xff1f;1.1.5.总结1.2.倒排索引1.2.1.正向索引1.2.2.倒排索引1.2.3.正向和倒排1.3.es的一些…

SpringMVC基本注解的使用和理解

SpringMVC基本注解的使用和理解 RequestParam注解 使用在方法入参位置&#xff0c;用于指定请求参数名称&#xff0c;将该请求参数绑定到注解参数位置。 属性&#xff1a;name:指定要绑定的请求参数名称&#xff1b; name属性和value属性互为别名。 required 和&#xff1a;指…

Java并发编程(8) —— AQS抽象同步队列详解

上一篇&#xff1a;Java并发编程(7) —— 锁的分类概述 在上一篇中我们提到并发包中的ReentrantLock类是一种可重入独占锁&#xff0c;其锁机制是基于AQS实现的。实际上&#xff0c;并发包java.util.concurrent.locks中的锁都是基于AQS 实现的。 一、AQS是什么 AbstractQueued…