zoukankan      html  css  js  c++  java
  • AQS底层原理分析讲解

    AQS 是什么
    在 Lock 中,用到了一个同步队列 AQS,全称 AbstractQueuedSynchronizer,它是一个同步工具也是 Lock 用来实现线程同步的核心组件。如果你搞懂了 AQS,那么 J.U.C 中绝大部分的工具都能轻松掌握。
    AQS 的两种功能
    从使用层面来说,AQS 的功能分为两种:独占和共享
    独占锁:每次只能有一个线程持有锁,比如前面给大家演示的 ReentrantLock 就是以独占方式实现的互斥锁。
    共 享 锁 : 允许多个线程同时获取锁 , 并发访问共享资源 , 比如ReentrantReadWriteLock。
    AQS 的内部实现
    AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成 Node 加入到 ASQ 队列中去;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。

    AQS核心变量

    1、volatile int state

    AQS使用一个 volatile 修饰的 int 变量来表示同步状态,当 state>0 时,表示已经获取到了锁,当 state=0 时,表示释放了锁。
    它提供了三个方法:
    getState()
    seState(int newState)
    compareAndSetState(int expect, int update)
    这三个方法用于对同步状态state进行操作,当然,AQS可以确保对state操作的安全性。

    2、FIFO同步队列
    AQS通过内置的FIFO同步队列,来完成资源获取线程的排队工作。
    如果当前线程获取同步状态失败时,AQS会将当前线程以及等待状态等信息,构造成一个节点(Node)并将其加入同步队列
    同时,会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。

    这个所谓的FIFO就是CLH队列:

    CLHNode的组成:

    释放锁以及添加线程对于队列的变化(通俗的讲就是入队和出队

    当出现锁竞争以及释放锁的时候,AQS 同步队列中的节点会发生变化,首先看一下添加节点的场景。

    入队源码:

      /**
         * Creates and enqueues node for current thread and given mode.
         * 新节点的创建并入队在制定模式下(共享式和独占式)
         * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
         * @return the new node
         */
          private Node addWaiter(Node mode) {
            //1、为当前线程创建新节点
            Node node = new Node(Thread.currentThread(), mode);
            // 尝试快速插入,如果失败则用enq插入
    
            //1、获取同步器中的tail指向的节点,即:未插入新节点时的尾节点(暂且称之为原队列中的尾节点)
            Node pred = tail;
            if (pred != null) {//判断尾节点不为空,为空则使用enq插入,enq中会存在创建head和Tail节点的逻辑
                //2、新节点的前驱节点指向原尾节点
                node.prev = pred;
                //3、使用CAS设置尾节点(AQS代码风格之一:将操作放入if判断中)
                if (compareAndSetTail(pred, node)) {
                    //4、将原尾节点的next指向新节点
                    pred.next = node;
                    return node;
                }
            }
            //如果快速插入队列失败,则用enq进行插入
            enq(node);
            return node;
        }
    
    
      /**
         * Inserts node into queue, initializing if necessary. See picture above.
         * 将节点插入队列,如果有必要(未节点为空),即:队列为空,则初始化队列
         * @param node the node to insert
         * @return node's predecessor
         */
        private Node enq(final Node node) {
            //类似节点获取同步状态时的自旋,其实就是有返回条件的死循环
            for (;;) {
                //1、将同步器中的未节点赋给临时变量t
                Node t = tail;
                if (t == null) { // Must initialize
                    //2、如果未节点为空,就是队列为空,则说明队列为空,新建一个节点,使用CAS设置头结点。,CAS保证操作的原子性
                    if (compareAndSetHead(new Node()))
                        //3、如果头结点设置成功,则将同步器中的未节点指向头结点,然后继续步骤1
                        tail = head;
                } else {
                    //4、将新节点的前驱节点指向原队列中的未节点
                    node.prev = t;
                    //5、使用CAS设置未节点,即:同步器中的tail指向新节点
                    if (compareAndSetTail(t, node)) {
                        //6、如果未节点设置成功,则将原未节点的next指向新节点
                        t.next = node;
                        return t;
                    }
                }
            }
        }

     这里会涉及到两个变化

    1. 新的线程封装成 Node 节点追加到同步队列中,设置 prev 节点以及修改当前节点的前置节点的 next 节点指向自己。
    2. 通过 CAS 将tail 重新指向新的尾部节点head 节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点,节点的变化过程如下:

    这个过程也是涉及到两个变化

    1. 修改 head 节点指向下一个获得锁的节点
    2. 新的获得锁的节点,将 prev 的指针指向 null设置 head 节点不需要用 CAS,原因是设置 head 节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要 CAS 保证,只需要把 head 节点设置为原首节点的后继节点,并且断开原 head 节点的 next 引用即可。
    AQS中最重要的就是aquire(int arg) 方法,它是AQS中提供的模板方法,该方法为独占式获取同步状态,会忽略中断,也就是说,由于线程获取同步状态失败加入到CLH同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移除。
    acquire方法流程图如下:
  • 相关阅读:
    线段树的一种简化实现[原] by 踏雪赤兔
    C 语言qsort 函数详解
    【转载】Amit’s A star Page 中译文
    Poj3468 线段树 成段更新
    在VMware的CentOS中运行Cadence IC 615的怪现象
    安装非认证的chrome插件和设置文件夹的权限
    [模拟IC]基础知识1
    如何删除 Windows.old 文件夹?
    美国签证面签经历
    三个不同属性的GTD工具
  • 原文地址:https://www.cnblogs.com/47Gamer/p/13086116.html
Copyright © 2011-2022 走看看