一、AQS基本介绍

同步器AbstractQueuedSynchronizer(简称AQS)是用来构建其他同步组件的基础,它使用了一个int变量来表示同步状态state,通过内置的FIFO队列来完成线程的排队工作

二、如何使用AQS来构建同步组件?

同步器的设计是基于模板模式的,使用者继承同步器并重写指定的方法。同步器可重写的方法如下:

方法名称描述
boolean tryAcquire(int arg)尝试独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态
boolean tryRelease(int arg)尝试独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
int tryAcquireShared(int arg)尝试共享式获取同步状态,返回大于等于0的值,表示获取成功,反之获取失败。
boolean tryReleaseShared(int arg)尝试共享式释放同步状态
boolean isHeldExclusively()当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程独占

实现自定义同步组件时,将会调用同步器提供的模板方法,如下:

方法名称描述
void acquire(int arg)独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则将会进入同步队列等待,该方法会调用重写的tryAcquire方法
void acquireInterruptibly(int arg)与acquire相同,但是该方法响应中断。如果当前线程未获取到同步状态而进入同步队列,如果当前线程被中断,则该方法会抛出InterruptedException异常
boolean tryAcquireNanos(int arg, long nanos)在acquireInterruptibly基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,则返回false,反之返回true
void acquireShared(int arg)共享式地获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式主要区别在于同一时刻可以有多个线程获取到同步状态
void acquireSharedInterruptibly(int arg)与acquireShared相同,该方法响应中断
boolean tryAcquiredSharedNanos(int arg, long nanos)在acquireSharedInterruptibly基础上增加了超时限制
boolean release(int arg)独占式的释放同步状态,该方法在释放同步状态之后,将同步队列的第一个节点的线程唤醒
boolean releaseShared(int arg)共享式的释放同步状态
Collection getQueuedThreads()获取等待在同步队列上的线程集合

三、AQS的实现分析

从实现角度来分析同步器是如何完成线程同步?

  1. 同步队列
  2. 独占式同步状态获取与释放
  3. 共享式同步状态获取与释放
  4. 超时获取同步状态

3.1 同步队列

AQS内部基于同步队列(一个双向队列)来完成同步状态的管理。当前线程获取同步状态失败时,会将当前线程以及等待状态等信息构造成为一个Node节点并将其加入同步队列,同时将当前线程挂起,当同步状态释放,会把首节点中的线程唤醒,使其再次获取同步状态。

Node类结构如下:

属性类型与名称描述
int waitStatus等待状态。1、CANCELLED,值为1,由于在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待,节点进入该状态将不会变化。2、SIGNAL,值为-1,后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或被取消,将会通知后继节点,使后继节点的线程得以运行。3、CONDITION,值为-2,节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中。4、PROPAGATE,值为-3,表示下一次共享式同步状态获取将会无条件的被传播下去。5、INITIAL,值为0,初始状态
Node prev前驱节点,当节点加入同步队列时被设置
Node next后继节点
Node nextWaiter等待队列(后续章节会介绍)中的后继节点。如果当前节点是共享的,那么这个字段是SHARED常量。也就是说节点类型(独占或共享)和等待队列中的后继节点共用同一个字段
Thread thread对应的线程

Node节点是构成同步队列的基础,没有成功获取同步状态的线程将会成为Node节点加入该队列的尾部,头节点是拥有同步状态的节点(如果是首次初始化,头节点会是空Node,这里只要记住头节点后面的节点都是在等待获取同步状态的节点)。同步队列的基本结构如下:
clipboard.png

3.2 独占式获取、释放同步状态

获取同步状态:通过调用同步器的acquire(int arg)方法获取同步状态,该方法无法响应中断。下述代码完成了同步状态获取、节点构造、加入同步队列等相关工作。

public final void acquire(int arg) {
    if(!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
}

流程如下:

  1. 调用自定义同步器的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果获取成功,则设置独占线程为当前线程,如果获取失败,构造节点并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部
  2. 接着调用acquireQueued(Node node, int arg)方法,使得节点再次尝试获取同步状态
  3. 如果获取不到会调用LockSupport.park()方法将当前线程挂起,而被挂起线程的唤醒主要依靠前驱节点的出队或线程被中断来实现。
    clipboard.png

释放同步状态:当前线程获取同步状态并执行了相应逻辑后,就需要释放同步状态,使得后续节点能够继续获取同步状态。通过调用同步器的release(int arg) 方法就可以释放同步状态,该方法在释放同步状态后,会唤醒其后继节点

public final boolean release(int arg) {
    if(tryRelease(arg)) {
        Node h = head;
        if(h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

总结:

  1. 获取同步状态时,同步器维护一个同步队列,获取状态失败的线程会被加入到队列尾部。这里会先判断一下前驱节点是否是头节点,如果是,则尝试获取同步状态,如果获取成功,直接将该节点设置为头节点,如果获取失败,将当前线程挂起。
  2. 释放同步状态时,同步器调用release(int arg)方法释放同步状态,唤醒头节点的后继节点

3.3 共享式获取、释放同步状态

通过调用同步器的acquireShared(int arg) 方法可以共享式地获取同步状态。

public final void acquireShared(int arg) {
    if(tryAcquireShared(arg) < 0) 
        doAcquireShared(arg);
}

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for(;;) {
            final Node p = node.predecessor();
            if(p == head) {
                int r = tryAcquireShared(arg);
                if(r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null;
                    if(interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if(shouldParkAfterFailedAcquire(p, node) 
                && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if(failed) {
            cancelAcquire(node);
        }
    }
}

在acquireShared(int arg)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状态,方法返回值为int值,如果大于等于0,表示能够获取到同步状态,反之无法获取同步状态。在doAcquireShared(int arg)方法的自旋过程中,如果当前节点的前驱为头节点,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功并从自旋过程中退出。

与独占式一样,共享式也需要释放同步状态,通过调用releaseShared(int arg)方法可以释放同步状态。它与独占式地主要区别在于 tryReealseShared(int arg)方法必须确保同步状态安全释放,一般是通过循环+CAS来保证的。

public final boolean releaseShared(int arg) {
    if(tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

3.4 超时获取同步状态

通过调用同步器的doAcquireNanos(int arg, long nanosTimeout)方法可以超时获取同步状态,在指定的时间段内获取同步状态,如果获取到则返回true,否则返回false。

超时获取同步状态过程响应中断获取同步状态过程的升级版,doAcquireNanos方法在支持响应中断的基础上,增加了超时获取的特性,该方法提供了synchronized关键字不具备的特性

独占式超时获取同步状态和独占式获取同步状态在流程上非常类似,主要区别在于未获取到同步状态时的处理逻辑。acquire(int arg)在未获取到同步状态时,将会使当前线程一直处于等待状态,而doAcquireNanos()方法会使当前线程等待nanosTimeout纳秒,如果当前线程在nanosTimeout纳秒内没有获取到同步状态,将会直接返回false。

四、总结

自定义同步组件可通过继承AQS并实现它的抽象方法来管理同步状态。同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,来实现不同类型的同步组件(如ReentrantLock可重入锁、ReentrantReadWriteLock读写锁和CountDownLatch锁存器)


kamier
1.5k 声望493 粉丝