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方法流程图如下: