AQS

以下内容可参考美团技术团队:从ReentrantLock的实现看AQS的原理及应用

讲到ReentrantLock就不得不讲AQS,因为Lock的底层就是基于AQS来实现的。那么。什么时AQS呢?

AQS全称AbstractQueuedSynchronizer,是JUC中的一个类。它提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。常见的ReentranrLock、Semaphore、CountDownLatch、ThreadPoolExecutor中都是用到了这个类。

AQS类源码简单解释

首先要看一下AQS类中的一些属性和方法及其作用。

属性:

  • state:同步状态,该属性用于表示当前锁是独占锁还是共享锁,也表示当前临界资源由多少个线程获得了锁。初始状态state=0;如果state=1表示为独占锁;如果state>1表示是共享锁。可以看一下ReentranrLockSemaphore在创建时时如何设置该属性的(以非公平锁为例)。

    ReentranrLock:该类中NonfairSync内部类最终是继承了AQS,在默认创建LOCK的时候就会创建这个类,所以只需要看这个类的源码即可。在调用lock()方法的时候就会对sta特进行设置,如下:

    final void lock() {
         // 使用CAS将0修改为1
         if (compareAndSetState(0, 1))
              setExclusiveOwnerThread(Thread.currentThread());
         else
              acquire(1);
    }
    

    Semaphore:在初始化的时候就会调用setState()方法进行设置,如下:

    // 构造方法
    Sync(int permits) {
          setState(permits);
    }   
    // 父类(AQS)中的setState方法
    protected final void setState(int newState) {
            state = newState;
    }
    
  • head:队列的头节点,在下一部分会讲到。

  • tail:队列的尾节点,在下一部分会讲到。

  • spinForTimeoutThreshold:自旋时间(纳秒)

  • 一些Offset:偏移量,不做详解。

方法:

在AQS中由非常多的类,这里只说一些比较常见的方法。

  • void acquire(int arg):获取锁资源方法,需要调用tryAcquire()方法,如果成功直接返回,如果失败则将该线程加入等待队列进行排队。
  • boolean tryAcquire(int arg):未实现方法需要实现类重写;尝试一次获取锁资源。
  • Node addWaiter(Node mode):将线程节点添加到等待队列中。
  • boolean acquireQueued(final Node node, int arg):以独占不间断模式获取已在队列中的线程。
  • cancelAcquire(Node node):取消正在获取锁资源的请求。
  • Node enq(final Node node):将就点加入到队列中,必要时初始化队列。
  • boolean release(int arg):释放锁资源,会调用tryRelease()方法
  • boolean tryRelease(int arg),为实现方法,需要实现类进行重写。
  • ......

内部类Node简单解释

在AQS中有一个非常重要的内部类Node类。顾名思义,表示节点。那么什么东西需要节点呢?这里就需要说到AQS中的一个CLH(Craig,Landin,and Hagersten)队列,他是一个虚拟的双端队列,使用Node进行连接。AQS中就有同步队列(Sync Queue)、和条件队列(Condition Queue)。条件队列在用到Condition时才使用,否则可不需要,这里不做介绍。

上面AQS的属性中讲到head、tail属性,head就是同步队列的头节点,tail就是同步队列的尾节点。注意:同步队列的头节点不存放任何有效信息,只是作为队列的初始化节点。

Node中的属性主要有如下:

        // 共享模式	
        static final Node SHARED = new Node();
       // 独占模式
        static final Node EXCLUSIVE = null;

        // 等待状态
		// 线程已取消,唯一大于0的状态值
        static final int CANCELLED =  1;
        // 表示后继线程需要unparking
        static final int SIGNAL    = -1;
        // 线程正在等待条件
        static final int CONDITION = -2;
        // 下一个 acquireShared 应该无条件传播
        static final int PROPAGATE = -3;
		// 当代状态,主要由上面四种
        volatile int waitStatus;

		// 前驱节点
        volatile Node prev;
		// 后继节点
        volatile Node next;
		// 节点包含的的线程
        volatile Thread thread;
		// 等待条件的下一个节点
        Node nextWaiter;

主要需要了解waitStatus属性的取值类型,且0为节点初始化默认值。

以实例的方式剖析原理——获取锁的过程

以下步骤可能比较繁琐,但是动手在IDE中一步一步走下来还是非常好理解的

第一层:ReentrantLock获取锁

ReentrantLock lock = new ReentrantLock();   // 创建ReentrantLock实例对象
// 获取锁
lock.lock();
try{
    // TODO
}

第二层:ReentrantLock类中的操作

通过源码可以知道,默认情况下创建的lock对象是一个非公平锁,所以这里就以非公平锁为例。至于ReentrantLock类的其他具体可参考源码。初始化如下:

// ReentrantLock构造方法
public ReentrantLock() {
    sync = new NonfairSync();    // 否公平锁
}

所以后续的lock操作可以说都是依赖于NonfairSync这个内部类进行操作,如下:

// 将会调用上面NonfairSync中的lock方法
public void lock() {
    sync.lock();
}

第三层:NonfairSync内部类的操作

// NonfairSync内部类中的方法
final void lock() {
    if (compareAndSetState(0, 1))
        // 获取锁成功,将当前线程设置为该锁的独占线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 否则,执行acquire逻辑
        acquire(1);
}

这里可以看到if中会使用CAS对state的状态进行设置(注意:NonfairSync类最终继承自AQS,所以继承有states属性)。state属性前面已经讲过——表示锁的状态,所以这里就是将0设置为1(1表示独占),如果成功表示获取锁成功并直接返回;否则将执行acquire()方法逻辑。

第四层:AQS的acquire()方法。

acquire()方法时AQS的核心方法之一,用于无视中断尝试获取锁资源。源码如下:

// AQS类中的qcquire方法
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&   // 尝试获取锁资源
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 添加节点到同步队列中
        selfInterrupt();
}

这里有两个逻辑,分别是tryAcquire()尝试获取锁资源,acquireQueued()让同步队列中的节点不断尝试获取锁资源。

如果tryAcquire()返回true表示获取锁资源成功,也就不需要执行后续操作。

第五层:tryAcquie()方法的实现

其中tryAcquire()是一个为实现方法,需要子类具体实现,可以看到NonfairSync的父类Sync是如何实现的。

// AQS中的tryAcquire未实现
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
// ReentrantLock类的内部类Sync中的方法对tryAcquire进行了具体实现
// 参数为1,依然表示独占锁
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();    // 获取同步状态
    if (c == 0) {          
        if (compareAndSetState(0, acquires)) {
             // 如果状态为0且CAS设置状态成功,则表示获取锁成功,返回true即可
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果状态不为0表示已有线程占有了锁,所以判断这个线程是不是当前线程,是则表示当前线程重入了,所以在原基础上+1即可,这也是lock可以如所实现的原理。
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 如果获取失败,则返回false,执行后续操作
    return false;
}

通过上面需要直到两点:

  1. 如果锁资源没有被占有,则tryAcquire会尝试CAS设置同步状态以达到获取锁的目的。
  2. 如果当前线程再次获取该锁资源,则将同步状态在原基础上+1即可,也就是lock可重入锁的实现原理

如果返回true则第四层中的&&后面操作将不再继续,否则执行后面的操作。

第六层:同步队列中的节点获取锁资源

如果第五步中的tryAcqiure()获取失败,则会执行到这一步。这里需要关注两个方法:acquireQueued()和addWaiter()方法。

先来看addWaiter()方法:

// AQS类中的addWaiter方法
// 参数为Node.EXCLUSIVE,即null,独占锁
private Node addWaiter(Node mode) {
    // 将当前线程封装为一个同步队列的节点并设置模式为独占式
    Node node = new Node(Thread.currentThread(), mode);
    // 获取并临时保存尾节点
    Node pred = tail;
    // 如果尾节点不为空,表示同步队列已存在
    if (pred != null) {
        // 将当前线程节点的前驱设置为尾节点
        node.prev = pred;
        // 使用CAS将当前线程的节点设置为尾节点
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 如果同步队列未初始化
    enq(node);
    return node;
}

///////////////////////   enq方法  ///////////////////////////
// 参数为当前线程所封装的节点
private Node enq(final Node node) {
    // 无限循环直到添加节点到同步队列成功
   for (;;) {
       Node t = tail;
       // 初始化同步队列
       if (t == null) { // Must initialize
           // 如果尾节点为空,则初始化一个空节点,并将其这是为头节点和尾节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {   // 添加当前线程的节点到同步队列的最后
             node.prev = t;
             if (compareAndSetTail(t, node)) {
                 t.next = node;
                 return t;
             }
         }
    }
}

通过上面的两个方法我们可以清楚的看到,这里的主要操作就是:将当前线程封装为Node节点并将其添加到同步队列的尾部,而enq()方法主要是应对同步队列不存的情况

再来看看acquireQueued()方法:

// AQS中的acquireQueued方法
// 参数:当前线程节点,1
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        // 中断标记
        boolean interrupted = false;
        for (;;) {
            // 获取当前线程节点的前驱节点
            final Node p = node.predecessor();
            // 如果前驱节点是头节点,则使当前线程尝试获取锁资源(tryAcquire方法忘了回头看第五步)
            if (p == head && tryAcquire(arg)) {
                // 如果当前程线程获取锁资源成功,则将当前线程节点设置为头节点
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 根据前驱节点p的等待状态判断是否要将当前线程阻塞
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 生成CANCELLED状态节点,并唤醒节点
        if (failed)
            cancelAcquire(node);
    }
}

该方法也是比较好理解:在将当前线程的节点添加到同步队列之后,判断当前线程是不是队列第二个节点(第一个节点不存放有效信息),则让当前线程尝试获取锁资源,成功则完事大吉,否则执行最后的尝试操作。

需要执行shouldParkAfterFailedAcquire方法和parkAndCheckInterrupt方法。

所以acquireQueued()方法的核心就是让同步队列中节点不断尝试获取锁资源

第七层:获取锁资源的最后底线

这里主要关注shouldParkAfterFailedAcquire方法和parkAndCheckInterrupt两个方法。

先看shouldParkAfterFailedAcquire()方法:

// AQS中的shouldParkAfterFailedAcquire方法
// 参数:当前线程节点的前驱节点,当前线程节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 获取前驱节点的等待状态   (忘了可见Node内部类的属性定义)
    int ws = pred.waitStatus;
    // 如果使SIGNAL状态,则进行park操作
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    // 如果线程为CANCELLED,则将其从同步队列中剔除
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            // 该行逻辑为:将当前线程的前一个节点设置为上上个节点,即跳过上一个节点
            node.prev = pred = pred.prev;  
        } while (pred.waitStatus > 0);  // 循环来看,就是跳过当前线程节点之前所有的CANCELLED节点
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        // 否则将前驱节点的等待状态设置为SIGAL,也就是能够park
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    // 返回false
    return false;
}

该方法主要操作是判断节点的等待状态,如下:

  • 判断当前线程的前驱节点的等待状态是否为SIGNAL,如果是则返回true并执行后面的parkAndCheckInterrupt()方法。
  • 判断当前线程的前驱节点的等待状态是否为CANCELLED,是则剔除当前线程节点之前所有的连续的该状态节点。
  • 以上都不是,则将前驱节点的等待状态设置为SIGNAL,最后返回false;

流程图如下(来自美团技术团队):

img

再来看parkAndCheckInterrupt()方法:

// AQS的parkAndCheckInterrupt方法
private final boolean parkAndCheckInterrupt() {
    // park当前线程
    LockSupport.park(this);
    return Thread.interrupted();
}

该方法很简单,就是park当前线程,即阻塞当前线程,并返回线程的中断状态。这一步的目的是:根据前驱节点的等待状态判断是否需要阻塞当前线程,避免无限循环浪费CPU资源。

到此基本的上锁操作完成,回到acquireQueued方法,逻辑如下图(来自美团技术团队):

img

不过还有一处地方需要思考:acquireQueued中的finally代码块,通过源码查看我们可以看到正常情况下finally中的cancelAcquire()一辈子不会执行,所以只有在异常情况下会执行,在异常情况下就会将当前线程的所资源请求操作取消并设置为CANCELLED状态。源码略。归结起来有三种情况:

  • 当前线程节点是尾节点:直接剔除当前节点;
  • 当前线程节点是既不是尾节点,也不是头节点的后继节点:略过当前节点,当前节点的前驱指向当前节点后继节点;
  • 当前节点是头节点的后继:唤醒当前节点之后最近的一个非CANCELLED状态的节点,实现方式是从尾节点开始向前遍历,找到最靠近的节点。

获取锁过程大总结:

  1. 首先使用lock.lock()的时候会调用NonfairSync中的lock方法,并尝试CAS这是同步状态获取锁资源;如果成功则获取锁成功,否则下一步。
  2. 调用AQS中的acquire()方法,该方法会先调用tryAcquire()方法尝试回去锁资源(该方法由AQS子类具体实现),这里会判断是否是当前线程的重入锁,成功直接返回,失败进入下一步。
  3. 获取失败之后会调用addWaiter()方法将当前线程添加到同步队列的尾部,然后调用acquireQueued()方法使同步队列中的节点获取锁资源:如果当前线程是head节点的后继节点(第二个节点)则尝试获取锁资源,成功则将当前节点设置为头节点并返回,否则下一步。
  4. 调用shouldParkAfterFailedAcquire()方法,如果当前节点的前驱是SIGNAL状态则调用parkAndCheckInterrupt()方法阻塞当前线程,如果是CANCELLED状态则去除当前节点之前的CANCELLED状态的节点,否则将前驱节点设置为SIGNAL,然后回到上一步再次尝试获取锁资源。

场景模拟:线程A先获取锁资源,线程B再获取锁资源,线程C最后获取锁资源。

  • 线程A先通过lock设置了state同步状态并获取到锁;
  • 线程B来获取锁,lock设置失败,调用acquire(),tryAcquire()失败,调用addWaiter()添加到同步队列,调用acquireQueued()方法,由于是head的后继节点则一尝试获取锁资源成功返回,否则调用shouldParkAfterFailedAcquire()方法将头节点这是为SIGNAL,下一次再执行这个方法的时候线程B就会被阻塞;
  • 线程C来获取锁,也进入到acquired()方法,但是它不是head的后继节点,所以直接调用shouldParkAfterFailedAcquire()方法将线程B设置为SIGNAL状态,下一次循环中调用parkAndCheckInterrupt()方法将线程C阻塞。

以实例的方式剖析原理——释放锁的过程

第一步:ReentrantLock释放锁

finally{
    lock.unlock();
}

接着会调用Sync类中的release()

public void unlock() {
    sync.release(1);  // 其实是父类AQS的方法
}

第二步:AQS中的release

// AQS中的release
// 参数:1
public final boolean release(int arg) {
    // 调用tryRelease尝试释放锁
    if (tryRelease(arg)) {
        Node h = head;
        // 如果头节点不为null且等待状态不为0
        if (h != null && h.waitStatus != 0)
            // 唤醒头结点的后继节点线程,将会调用LockSupport.unpark()
            unparkSuccessor(h);
        return true;
    }
    return false;
}

tryRelease释放锁:

与acquire()类似,AQS中的tryRelease()也没有实现,需要子类实现,看到Sync中的tryRelease重写方法:

// Sync重写方法
// 参数:1
protected final boolean tryRelease(int releases) {
    // 释放锁,所以要将同步状态-1
    int c = getState() - releases;
    // 如果不是当前线程则抛出异常,监视器状态异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 如果为0,没有线程占有锁
    if (c == 0) {
        free = true;
        // 设置独占线程为null
        setExclusiveOwnerThread(null);
    }
    // 设置新的同步状态
    setState(c);
    return free;
}

综上,可以看到释放锁的过程还是比较简单的:当修改完同步状态成功之后,只需要判断头节点的状态即可唤醒线程。

问题1:为什么要是(h != null && h.waitStatus != 0)?

  • h != null 好理解,防止空指针;
  • h.waitStatus != 0 呢,其实当一个线程被new为一个Node的时候waitStatus默认就是0,只有在获取锁的时候调用了shouldParkAfterFailedAcquire()方法之后会将其设置为SIGNAL。所以可以理解,当waitStatus==0的时候其后继节点一定没有被park,所以也就不需要唤醒了。否则就需要执行唤醒操作。

问题2:tryRelease()中的c会大于1吗?

  • 会,这就是可重入锁的概念,一个线程重入了n次,c就会是n。因此只有c==0的时候才会返回true表示释放锁资源完成。

以上就是ReentrantLock中使用AQS实现获取锁释放锁的大致过程,更多内容好需要进一步仔细研读源码。

posted on 2022-01-17 20:14  wuraoo  阅读(549)  评论(0编辑  收藏  举报