当前位置: 首页 > news >正文

Java并发之AQS详解

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

         AQS是AbstractQueuedSynchronizer的简称。AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。AQS为一系列同步器依赖于一个单独的原子变量(state)的同步器提供了一个非常有用的基础。子类们必须定义改变state变量的protected方法,这些方法定义了state是如何被获取或释放的。鉴于此,本类中的其他方法执行所有的排队和阻塞机制。子类也可以维护其他的state变量,但是为了保证同步,必须原子地操作这些变量。

         介绍下FIFO的等待队列,这是一个双向队列,主要作用是用来存放在锁上阻塞的线程,当一个线程尝试获取锁时,如果已经被占用,那么当前线程就会被构造成一个Node节点加入到同步队列的尾部,队列的头节点是成功获取锁的节点,当头节点线程释放锁时,会唤醒后面的节点并释放当前头节点的引用。 

        如图:

        7d19022aefa08536160faea51e0a5496bc6.jpg

独占锁的获取和释放流程

获取锁

1、调用入口方法acquire(arg)
2、调用模版方法tryAcquire(arg)尝试获取锁,若成功则返回,若失败则走下一步
3、将当前线程构造成一个Node节点,并利用CAS将其加入到同步队列到尾部,然后该节点对应到线程进入自旋状态
4、自旋时,首先判断其前驱节点释放为头节点&是否成功获取同步状态,两个条件都成立,则将当前线程的节点设置为头节点,如果不是,则利用LockSupport.park(this)将当前线程挂起 ,等待被前驱节点唤醒

释放锁

1、调用入口方法release(arg)

2、调用模版方法tryRelease(arg)释放同步状态

3、获取当前节点的下一个节点

4、利用LockSupport.unpark(currentNode.next.thread)唤醒后继节点(接获取的第四步)

共享锁的获取和释放流程

获取锁

1、调用acquireShared(arg)入口方法

2、进入tryAcquireShared(arg)模版方法获取同步状态,如果返返回值>=0,则说明同步状态(state)有剩余,获取锁成功直接返回

3、如果tryAcquireShared(arg)返回值<0,说明获取同步状态失败,向队列尾部添加一个共享类型的Node节点,随即该节点进入自旋状态

4、自旋时,首先检查前驱节点释放为头节点&tryAcquireShared()是否>=0(即成功获取同步状态)

5、如果是,则说明当前节点可执行,同时把当前节点设置为头节点,并且唤醒所有后继节点

6、如果否,则利用LockSupport.unpark(this)挂起当前线程,等待被前驱节点唤醒

释放锁

1、调用releaseShared(arg)模版方法释放同步状态

2、如果释放成,则遍历整个队列,利用LockSupport.unpark(nextNode.thread)唤醒所有后继节点

独占锁和共享锁在实现上的区别

独占锁的同步状态值为1,即同一时刻只能有一个线程成功获取同步状态

共享锁的同步状态>1,取值由上层同步组件确定

独占锁队列中头节点运行完成后释放它的直接后继节点

共享锁队列中头节点运行完成后释放它后面的所有节点

共享锁中会出现多个线程(即同步队列中的节点)同时成功获取同步状态的情况

重入锁

重入锁指的是当前线成功获取锁后,如果再次访问该临界区,则不会对自己产生互斥行为。Java中对ReentrantLock和synchronized都是可重入锁,synchronized由jvm实现可重入即使,ReentrantLock都可重入性基于AQS实现。

同时,ReentrantLock还提供公平锁和非公平锁两种模式。

首先来看看ReentrantLock的构造方法,它的构造方法有两个,如下图所示:
QQå¾ç20140110194534

很显然,对象中有一个属性叫sync,有两种不同的实现类,默认是“NonfairSync”来实现,而另一个“FairSync”它们都是排它锁的内部类,不论用那一个都能实现排它锁,只是内部可能有点原理上的区别。先以“NonfairSync”类为例,实现lock()方法。

QQå¾ç20140110194615

lock()方法先通过CAS尝试将状态从0修改为1。若直接修改成功,前提条件自然是锁的状态为0,则直接将线程的OWNER修改为当前线程,这是一种理想情况,如果并发粒度设置适当也是一种乐观情况。
若上一个动作未成功,则会间接调用了acquire(1)来继续操作,这个acquire(int)方法就是在AbstractQueuedSynchronizer当中了。
首先看tryAcquire(arg)这里的调用(当然传入的参数是1),在默认的“NonfairSync”实现类中,会这样来实现:

QQå¾ç20140110194650

○ 首先获取这个锁的状态,如果状态为0,则尝试设置状态为传入的参数(这里就是1),若设置成功就代表自己获取到了锁,返回true了。状态为0设置1的动作在外部就有做过一次,内部再一次做只是提升概率,而且这样的操作相对锁来讲不占开销。
○ 如果状态不是0,则判定当前线程是否为排它锁的Owner,如果是Owner则尝试将状态增加acquires(也就是增加1),如果这个状态值越界,则会抛出异常提示,若没有越界,将状态设置进去后返回true(实现了类似于偏向的功能,可重入,但是无需进一步征用)。
○ 如果状态不是0,且自身不是owner,则返回false。

对tryAcquire()的调用判定中是通过if(!tryAcquire())作为第1个条件的,如果返回true,则判定就不会成立了,自然后面的acquireQueued动作就不会再执行了,如果发生这样的情况是最理想的。
无论多么乐观,征用是必然存在的,如果征用存在则owner自然不会是自己,tryAcquire()方法会返回false,接着就会再调用方法:acquireQueued(addWaiter(Node.EXCLUSIVE), arg)做相关的操作。
这个方法的调用的代码更不好懂,需要从里往外看,这里的Node.EXCLUSIVE是节点的类型,看名称应该清楚是排它类型的意思。接着调用addWaiter()来增加一个排它锁类型的节点,这个addWaiter()的代码是这样写的:

/**
     * 把Node节点添加到同步队列的尾部
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);  // 以独占模式把当前线程封装成一个Node节点
        // 尝试快速入队
        Node pred = tail;  // 当前队列的尾节点赋给pred
        if (pred != null) {  // 先觉条件 尾节点不为空
            node.prev = pred;  // 把pred作为node的前继节点
            if (compareAndSetTail(pred, node)) { //利用CAS把node作为尾节点
                pred.next = node;    // 把node作为pred的后继节点
                return node;       // 直接返回node
            }
        }
        enq(node);  // 尾节点为空或者利用CAS把node设为尾节点失败
        return node;
    }
    /**
     * 采用自旋的方式把node插入到队列中
     */
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // 如果t为空,说明队列为空,必须初始化
                if (compareAndSetHead(new Node())) // 新建一个节点利用CAS设为头节点,就是这样的形式 head=tail=null
                    tail = head;
            } else {    // 尾节点不为空的情况
                node.prev = t;  // 把t设为node的前驱节点
                if (compareAndSetTail(t, node)) {  // 利用CAS把node节点设为尾节点
                    t.next = node;   // 更改指针  把node作为t的后继节点
                    return t;   // 直接返回t
                }
            }
        }
    }

这里创建了一个Node的对象,将当前线程和传入的Node.EXCLUSIVE传入,也就是说Node节点理论上包含了这两项信息。代码中的tail是AQS的一个属性,刚开始的时候肯定是为null,也就是不会进入第一层if判定的区域,而直接会进入enq(node)的代码,那么直接来看看enq(node)的代码。

看到了tail就应该猜到了AQS是链表吧,没错,而且它还应该有一个head引用来指向链表的头节点,AQS在初始化的时候head、tail都是null,在运行时来回移动。此时,我们最少至少知道AQS是一个基于状态(state)的链表管理方式。

这段代码就是链表的操作。
首先这个是一个死循环,而且本身没有锁,因此可以有多个线程进来,假如某个线程进入方法,此时head、tail都是null,自然会进入if(t == null)所在的代码区域,这部分代码会创建一个Node出来名字叫h,这个Node没有像开始那样给予类型和线程,很明显是一个空的Node对象,而传入的Node对象首先被它的next引用所指向,此时传入的node和某一个线程创建的h对象如下图所示。

转载于:https://my.oschina.net/maojindaoGG/blog/3050104

相关文章:

  • htaccess隐藏index.php,301重定向等等..
  • CF241B Friends
  • Git学习总结——简单易懂的教程
  • 整理收集的一些常用java工具类
  • vue+express+mysql +node项目搭建
  • AGC002 补题小结
  • 现代前端不切图
  • Nginx反向代理后应用程序获取客户端真实IP
  • PHP正则匹配中文
  • [转] Batch Normalization
  • tortoiseGit did not exit cleanly (exit code 128)
  • 小猿圈web前端开发之什么是HTTPS
  • 1004 成绩排名 (20 分)
  • 习题4-6 水仙花数
  • odoo10同一模型的不同视图不同群组权限控制
  • 时间复杂度分析经典问题——最大子序列和
  • 【翻译】Mashape是如何管理15000个API和微服务的(三)
  • 2018天猫双11|这就是阿里云!不止有新技术,更有温暖的社会力量
  • codis proxy处理流程
  • markdown编辑器简评
  • Netty+SpringBoot+FastDFS+Html5实现聊天App(六)
  • 阿里云ubuntu14.04 Nginx反向代理Nodejs
  • 基于axios的vue插件,让http请求更简单
  • 极限编程 (Extreme Programming) - 发布计划 (Release Planning)
  • 经典排序算法及其 Java 实现
  • 漂亮刷新控件-iOS
  • 使用SAX解析XML
  • 数组的操作
  • 通过获取异步加载JS文件进度实现一个canvas环形loading图
  • 想写好前端,先练好内功
  • 用Python写一份独特的元宵节祝福
  • 自制字幕遮挡器
  • ​插件化DPI在商用WIFI中的价值
  • ​力扣解法汇总1802. 有界数组中指定下标处的最大值
  • #我与Java虚拟机的故事#连载15:完整阅读的第一本技术书籍
  • (Forward) Music Player: From UI Proposal to Code
  • (Matalb回归预测)PSO-BP粒子群算法优化BP神经网络的多维回归预测
  • (pytorch进阶之路)扩散概率模型
  • (二)Eureka服务搭建,服务注册,服务发现
  • (二)JAVA使用POI操作excel
  • (蓝桥杯每日一题)平方末尾及补充(常用的字符串函数功能)
  • (每日持续更新)信息系统项目管理(第四版)(高级项目管理)考试重点整理第3章 信息系统治理(一)
  • (三)c52学习之旅-点亮LED灯
  • (三分钟了解debug)SLAM研究方向-Debug总结
  • (十七)devops持续集成开发——使用jenkins流水线pipeline方式发布一个微服务项目
  • (数据结构)顺序表的定义
  • (转)PlayerPrefs在Windows下存到哪里去了?
  • . Flume面试题
  • .360、.halo勒索病毒的最新威胁:如何恢复您的数据?
  • .Net - 类的介绍
  • .NET 6 Mysql Canal (CDC 增量同步,捕获变更数据) 案例版
  • .net 7 上传文件踩坑
  • .NET Core实战项目之CMS 第十二章 开发篇-Dapper封装CURD及仓储代码生成器实现
  • .NET delegate 委托 、 Event 事件,接口回调
  • .NET关于 跳过SSL中遇到的问题