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方法流程图如下:
posted @ 2020-06-10 16:55  47号Gamer丶  阅读(901)  评论(0编辑  收藏  举报