深入理解AQS(抽象队列同步器)

1. 前言

        抽象队列同步器AbstractQueuedSynchronizer,是用来构建java.util.concurrent包中的锁和其他并发相关的组件的基础类。它通过使用一个int类型的state来表示同步状态,将没有获取锁的线程构造成一个节点,放到一个先进先出队列里,这个队列是使用链表实现的。当一个线程释放锁后,会将这个队列里的节点对应的线程唤醒。

        AQS是抽象类,所以使用时需要继承AQS,实现所有protected方法。子类应该定义为非public的内部帮助类(helper class),用来实现外部类的同步属性。

2. AQS中的主要方法

        AQS是基于模板方法模式设计的,所以使用时需要继承AQS并重写指定的方法。然后将AQS组合在自定义同步组件的实现中,并调用AQS提供的模板方法,而这些模板方法将会调用重写的方法。

2.1 子类可重写的方法

重写AQS指定的方法时,需要使用AQS提供的下面3个方法来获取,修改同步状态。

  • getState():获取当前同步状态
  • setState():设置当前同步状态
  • compareAndSetStatus(int expect, int update):使用CAS设置同步状态,该方法可以保证原子性

同步器可重写的方法如下:

方法

描述

protected protected boolean tryAcquire(int arg)

尝试独占式获取同步状态。这个方法应该先检查对象的状态是否允许独占式获取,如果是将获取同步状态

protected boolean tryRelease(int arg)

尝试独占式设置状态以释放锁

protected int tryAcquireShared(int arg)

尝试共享式获取同步状态。这个方法应该先检查对象的状态是否允许共享式获取,如果是将获取同步状态。

返回负数表示失败,其他表示成功

返回值详细说明:

a negative value on failure; zero if acquisition in shared mode succeeded but no subsequent shared-mode acquire can succeed; and a positive value if acquisition in shared mode succeeded and subsequent shared-mode acquires might also succeed, in which case a subsequent waiting thread must check availability. (Support for three different return values enables this method to be used in contexts where acquires only sometimes act exclusively.) Upon success, this object has been acquired.

protected boolean tryReleaseShared(int arg)

尝试共享式设置状态以释放锁

protected boolean isHeldExclusively()

当前线程是否在独占模式下占用同步状态(synchronization)

2.2 模板方法

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

方法

描述

public final void acquire(int arg)

独占式获取同步状态,忽略中断。通过至少一次调用tryAcquire方法实现

如果获取成功,则会返回,否则,线程将会进入同步队列,可能会重复地阻塞,非阻塞(unblocking),调用tryAcquire,直到成功

public final void acquireInterruptibly(int arg)

在acquire(int arg)基础上增加了中断响应

public final boolean tryAcquireNanos(int arg, long nanosTimeout)

在acquireInterruptibly(int arg)基础上增加了超时限制

public final void acquireShared(int arg)

共享式获取同步状态,忽略中断。通过先至少调用一次tryAcquireShared实现。与独占式获取的主要区别是在同一时刻可以有多个线程获取到同步状态

public final void acquireSharedInterruptibly(int arg)

在acquireShared(int arg)基础上增加了中断响应

public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)

在acquireSharedInterruptibly(int arg)基础上增加了超时限制

public final boolean release(int arg)

独占式释放同步状态。该方法会在释放同步状态后,将同步队列中第一个节点包含的线程唤醒

public final boolean releaseShared(int arg)

共享式释放同步状态

public final Collection getQueuedThreads()

获取等待在同步队列上的线程集合

        上面一共有三类方法:独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的等待线程情况。

3. 代码示例

        下面这个类是官方给的示例。互斥(独占)锁是一个自定义同步组件,它在同一时刻只允许一个线程占有锁。

/**
 * Here is a non-reentrant mutual exclusion lock class that uses the value zero to represent the unlocked state,
 * and one to represent the locked state. While a non-reentrant lock does not strictly require recording of the current owner thread,
 * this class does so anyway to make usage easier to monitor.
 * It also supports conditions and exposes one of the instrumentation methods:
 * 非重入的互斥锁,0表示非锁定状态,1表示锁定状态。
 * 非重入锁不严格要求记录当前所有者线程,总之这个类使监视器的使用更加简单。
 * 它还支持条件,并暴露一种检测的方法。
 */
public class Mutex implements Lock, Serializable {

    private static class Sync extends AbstractQueuedSynchronizer{
        // Acquires the lock if state is zero
        //如果状态是0的话,获取锁
        @Override
        protected boolean tryAcquire(int acquires) {
            assert acquires==1;
            if(compareAndSetState(0,1)){
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        //Releases the lock by setting state to zero
        //通过将state设置为0来释放锁
        @Override
        protected boolean tryRelease(int releases) {
            assert releases==1;
            if(getState()==0) throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        // Reports whether in locked state
        //是否在占用(独占)状态
        @Override
        protected boolean isHeldExclusively() {
            return getState()==1;
        }
        // Provides a Condition
        //提供一个condition,每个condition都包含了一个condition队列
        Condition newCondition(){
            return new ConditionObject();
        }
        // Deserializes properly 反序列化
        private void readObject(ObjectInputStream s)
                throws IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }
    /**
     * 仅需要将操作代理到Sync上即可
     */
    private final Sync sync=new Sync();

    @Override
    public void lock() {
        sync.acquire(1);
    }
    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1,unit.toNanos(time));
    }
    @Override
    public void unlock() {
        sync.release(1);
    }
    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
    public boolean hasQueuedThreads(){
        return sync.hasQueuedThreads();
    }
    public boolean isLocked(){
        return sync.isHeldExclusively();
    }
}

4. AQS的实现

4.1 同步队列

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

4.1.1 节点的数据结构

static final class Node {
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;
    
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
    
    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    Node nextWaiter;
    final boolean isShared() {
        return nextWaiter == SHARED;
    }
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }
    Node() {    // Used to establish initial head or SHARED marker}
    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode; this.thread = thread;
    }
    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus; this.thread = thread;
    }
}

主要属性描述如下

waitStatus

获取同步状态失败后,会有以下几种状态

SIGNAL:当前节点的后继节点处于阻塞状态,所以当前节点释放同步状态或者被取消时,必须唤醒(unpark)它的后继节点

CANCELLED:由于超时或者中断,节点被取消

CONDITION:节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()后,该节点将会从等待队列中转移到同步队列中,此时状态变为0

PROPAGATE:表示下一次 共享式同步状态获取 将会被传播给其他节点

0:初始状态

volatile Node prev;

前驱节点

volatile Node next;

后继节点

volatile Thread thread;

节点对应的线程

Node nextWaiter;

等待队列中的后继节点

4.1.2 同步队列的基本结构

4.1.3 头尾节点设置

设置尾节点:

        当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法:

private final boolean compareAndSetTail(Node expect, Node update) {
    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}

设置头结点:

        设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证,它只需要将头节点设置成为原头节点的后继节点并断开原首节点的next引用即可。

5. 同步状态获取与释放

以独占式获取与释放同步状态为例讲解

        通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出。

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

        上述代码主要完成了同步状态获取、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作,其主要逻辑是:首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

如果CAS设置尾节点失败,则调用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;
            }
        }
    }
}

        在enq(final Node node)方法中,同步器通过“死循环”来保证节点的正确添加,在“死循环”中只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线程不断地尝试设置。可以看出,enq(final Node node)方法将并发添加节点的请求通过CAS变得“串行化”了。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

        在acquireQueued(final Node node,int arg)方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态,这是为什么?原因有两个,如下

第一,头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。

第二,维护同步队列的FIFO原则。

        如果获取同步状态失败的话,会检查是否应该阻塞该线程,如果可以阻塞的话,线程将会阻塞,否则会继续循环尝试获取同步状态。

当前节点的前驱节点等待状态为SIGNAL的话,就应该阻塞当前节点。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        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.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

        由于非头节点线程前驱节点出队或者被中断而从等待状态返回,随后检查自己的前驱是否是头节点,如果是则尝试获取同步状态。可以看到节点和节点之间在循环检查的过程中基本不相互通信,而是简单地判断自己的前驱是否为头节点,这样就使得节点的释放规则符合FIFO,并且也便于对过早通知的处理(过早通知是指前驱节点不是头节点的线程由于中断而被唤醒)。

独占式同步状态获取流程,也就是acquire(int arg)方法调用流程如图所示:

        当同步状态获取成功之后,当前线程从acquire(int arg)方法返回,如果对于锁这种并发组件而言,代表着当前线程获取了锁。

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

独占式获取与释放同步状态总结:

        在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。

6. condition接口

        任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。

6.1 condition接口与Object的监视器方法对比

对比项

Object Monitor Methods

Condition

前置条件

获取对象锁

synchronized

调用Lock.lock()获取锁

调用Lock.newConditon()获取condition对象

调用方式

直接调用

object.wait();

直接调用

condition.await();

等待队列个数

一个

多个

当前线程释放锁进入等待状态

支持

public final void wait() throws InterruptedException;

支持

void await() throws InterruptedException;

当前线程释放锁进入等待状态,在等待状态中不响应中断

不支持

支持

void awaitUninterruptibly();

当前线程释放锁进入超时等待状态

支持

public final native void wait(long timeout) throws InterruptedException;

支持

long awaitNanos(long nanosTimeout);

当前线程释放锁进入等待状态到将来的某个时间

不支持

支持

boolean awaitUntil(Date deadline) throws InterruptedException;

唤醒等待队列中的一个线程

支持

public final native void notify();

支持

void signal();

唤醒等待队列中的全部线程

支持

public final native void notifyAll();

支持

void signalAll();

调用notify和signal及其他唤醒方法后,如果等待的线程没有获取锁,则等待的线程仍会等待

6.2 condition实现原理

        ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队列(以下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。

6.2.1 等待队列

        等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。事实上,节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类AbstractQueuedSynchronizer.Node。

同步队列与等待队列如下图所示:

        Condition拥有头尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter指向它,并且更新尾节点即可。上述节点引用更新的过程并没有使用CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。

6.2.2 等待

        调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    //将当前线程加入等待队列    
    Node node = addConditionWaiter();
    //释放同步状态(锁)
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

将当前线程构造成一个新节点,加入到等待队列中。

/**
 * Adds a new waiter to wait queue.
 * @return its new wait node
 */
private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

6.2.3 通知

        调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}
private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
    /*
     * If cannot change waitStatus, the node has been cancelled.
     */
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    /*
     * Splice onto queue and try to set waitStatus of predecessor to
     * indicate that thread is (probably) waiting. If cancelled or
     * attempt to set waitStatus fails, wake up to resync (in which
     * case the waitStatus can be transiently and harmlessly wrong).
     */
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

        调用该方法的前置条件是当前线程必须获取了锁,可以看到signal()方法进行了isHeldExclusively()检查,也就是当前线程必须是获取了锁的线程。接着将等待队列的首节点移动到同步队列并使用LockSupport唤醒节点中的线程。

        被唤醒后的线程,将从await()方法中的while循环中退出(isOnSyncQueue(Node node)方法返回true,节点已经在同步队列中),进而调用同步器的acquireQueued()方法加入到获取同步状态的竞争中。

        成功获取同步状态(或者说锁)之后,被唤醒的线程将从先前调用的await()方法返回,此时该线程已经成功地获取了锁。

Condition的signalAll()方法,将等待队列里的节点全部移除,将它们添加到同步队列中,并唤醒每个节点。

/**
 * Removes and transfers all nodes.
 * @param first (non-null) the first node on condition queue
 */
private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}

参考资料:

1.《Java并发编程的艺术》

2. Class AbstractQueuedSynchronizer
Java Platform SE 8https://docs.oracle.com/javase/8/docs/api/index.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值