【JAVA并发编程】AbstractQueuedSynchronizer(AQS)的实现原理

一、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节点是构成同步队列的基础,没有成功获取同步状态的线程将会成为节点加入该队列的尾部,头节点是拥有同步状态的节点。同步队列的基本结构如下:
【JAVA并发编程】AbstractQueuedSynchronizer(AQS)的实现原理_第1张图片

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()方法阻塞当前线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
    【JAVA并发编程】AbstractQueuedSynchronizer(AQS)的实现原理_第2张图片

释放同步状态:当前线程获取同步状态并执行了相应逻辑后,就需要释放同步状态,使得后续节点能够继续获取同步状态。通过调用同步器的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锁存器)

你可能感兴趣的:(java后端)