AQS是什么
AQS是抽象的队列同步器,是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量 表示持有锁的状态。
AQS为什么是JUC内容中最重要的基石?
AQS的作用
当我们给资源加锁时,则会导致线程阻塞,有阻塞就需要排队,实现排队则需要一个队列。抢到资源的线程直接使用处理业务,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node),通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果。
AQS底层实现
AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的 FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成 一个Node节点来实现锁的分配,通过CAS完成对State值的修改,State为0时表示资源没有被任何线程所占用,可以获取,1表示资源已经被占用,则后来的线程需要到队列中等待。
AQS的CLH队列
CLH队列为一个双向队列,每个node节点都是一个正在等待的线程,线程从尾部入队,头部出队,在队列中通过CAS自旋等待。
Node节点的内部结构
各属性的说明
通过ReentrantLock解读AQS
ReentrantLock支持公平锁和非公平锁,默认使用非公平锁。
对比公平锁和非公平锁的 tryAcquire()方法的实现代码,其实差别就在于非公平锁获取锁时比公平锁中少了一个判断 !hasQueuedPredecessors()
hasQueuedPredecessors() 中判断了是否需要排队,导致公平锁和非公平锁的差异如下:
公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;
非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程在unpark(),之后还是需要竞争锁(存在线程竞争的情况下)
ReentrantLock.lock()
当第一个线程(线程A)调用lock()方法后,会通过CAS的方式将state设置为1,也就是资源已经处于被占用状态,并且通过setExclusiveOwnerThread()方法将当前占用线程设置为线程A。后续线程来获取资源时则需要通过acquire()方法传递一个默认值1来尝试获取。
第二个线程(线程B)通过tryAcquire()方法来尝试获取锁,而tryAcquire()方法中调用了非公平锁的获取方式nofairTryAcquire()。
nofairTryAcquire()中先获取当前线程(线程B),并通过getState()方法获取资源现在的状态,如果为0,则说明资源此时未被任何线程占用,那么就通过cas将state再次设置为1,并将当前线程(线程B)设置为占用线程。
如果state为1,则判断当前线程(线程B)是否为正在占用资源的线程(线程A),如果不是,则获取资源失败。
当线程B获取资源失败之后,则会被放入CLH队列中自旋等待,而将线程B放入队列的操作则通过addWaiter()方法实现。addWaiter()会传入一个默认的节点状态,在该方法中为线程B新建一个节点,再查看当前队列的尾指针是否为null,如果为null,则说明线程B是第一个进入队列的线程,则会通过enq()方法为该队列创建一个哨兵节点作为储存线程B的这个节点的前置节点以及头节点。
在enq()方法中有一个无限循环,在循环中先查看当前队列的尾节点,如果为null,则通过CAS创建一个新的节点,将该节点作为头节点,也就是刚刚说的哨兵节点,并将尾指针和头指针都指向该哨兵节点。再次进入循环时由于此时的尾指针指向了哨兵节点,不再为null,则将装有B线程的节点(B节点)的前置节点设置为哨兵节点,通过CAS将尾指针指向B节点,然后返回B节点,将节点传递给acquireQueued()方法。
注意:哨兵节点的作用仅仅为占位,不储存任何信息。
acquireQueued()接收B节点已经一个默认值1,在该方法中也有一个无限循环,先获取B节点的前置节点,查看是否为头节点,因为该队列为FIFO队列,所以有着先进先出原则,所以需要查看该节点是否为最先进入队列的节点。此时再尝试让B线程去抢占资源,如果抢占成功则通过setHead()方法将B节点设置为头节点,也就是新的哨兵节点,该方法会将节点的状态以及信息修改,不储存任何信息。如果抢占失败,则通过shouldParkAfterFailAcquire()方法将B线程最终存储到B节点中,并通过parkAndCheckInterrupt()将该线程挂起。
shouldParkAfterFailedAcquire()方法会判断B节点的前驱节点的状态,如果为SIGNAL状态,则返回true,后续执行parkAndCheckInterrupt()方法挂起线程,否则通过CAS将前驱节点的状态改为SIGNAL返回false,外层再次循环进入该方法,执行上述步骤。
parkAndCheckInterrupt()方法则使用LockSupport.park()方法将B线程挂起。