多线程学习-队列同步器

前言

AbstractQueuedSynchronizer,即队列同步器,通过继承AbstractQueuedSynchronizer并重写其方法可以实现锁或其它同步组件,本篇文章将对AbstractQueuedSynchronizer的使用和原理进行学习。

参考资料:《Java并发编程的艺术》

正文

一. AbstractQueuedSynchronizer的使用

AbstractQueuedSynchronizer的使用通常如下。

  • 创建AbstractQueuedSynchronizer的子类作为同步组件(例如ReentrantLockCountDownLatch等)的静态内部类并重写AbstractQueuedSynchronizer规定的可重写的方法;
  • 同步组件通过调用AbstractQueuedSynchronizer提供的模板方法来实现同步组件的同步功能。

先对AbstractQueuedSynchronizer的可重写的方法进行说明。AbstractQueuedSynchronizer是基于模板设计模式来实现锁或同步组件的,AbstractQueuedSynchronizer内部维护着一个字段叫做state,该字段表示同步状态,是一个整型变量,AbstractQueuedSynchronizer规定了若干方法来操作state字段,但AbstractQueuedSynchronizer本身并没有对这些方法进行实现,而是要求AbstractQueuedSynchronizer的子类来实现这些方法,下面看一下这些方法的签名和注释。

方法签名 注释
protected boolean tryAcquire(int arg) 独占式地获取同步状态。该方法在独占式获取同步状态以前应该判断是否允许独占式获取,如果允许则尝试基于CAS方式来设置同步状态,设置成功则表示获取同步状态成功。
protected boolean tryRelease(int arg) 独占式地释放同步状态。可以理解为将同步状态还原为获取前的状态。
protected int tryAcquireShared(int arg) 共享式地获取同步状态。
protected boolean tryReleaseShared(int arg) 共享式地释放同步状态。
protected boolean isHeldExclusively() 判断当前线程是否独占当前队列同步器。

实际上,AbstractQueuedSynchronizer规定的这些可重写的方法,均会被AbstractQueuedSynchronizer提供的模板方法所调用,在基于AbstractQueuedSynchronizer实现同步组件时,可根据同步组件的实际功能来重写这些可重写方法,然后再通过调用模板方法来实现同步组件的功能。下面看一下AbstractQueuedSynchronizer提供的模板方法。

方法签名 注释
public final void acquire(int arg) 独占式获取同步状态,即独占式获取锁。获取成功则该方法返回,获取失败则当前线程进入同步队列等待。
public final void acquireInterruptibly(int arg) throws InterruptedException 独占式获取同步状态,并响应中断。即如果获取同步状态失败,则会进入同步队列等待,此时如果线程被中断,则会退出等待状态并抛出中断异常。
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException acquireInterruptibly(int arg),并在其基础上指定了等待时间,若超时还未获取同步状态则返回false
public final void acquireShared(int arg) 共享式获取同步状态,即共享式获取锁。获取成功则该方法返回,获取失败则当前线程进入同步队列等待,支持同一时刻多个线程获取到同步状态。
public final void acquireSharedInterruptibly(int arg) throws InterruptedException 共享式获取同步状态,并响应中断。
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException acquireSharedInterruptibly(int arg),并在其基础上指定了等待时间,若超时还未获取同步状态则返回false
public final boolean release(int arg) 独占式地释放同步状态,即独占式地释放锁。成功释放锁之后会将同步队列中的第一个节点的线程唤醒。
public final boolean releaseShared(int arg) 共享式地释放同步状态。
public final Collection getQueuedThreads() 获取在同步队列上等待地线程。

下面结合《Java并发编程的艺术》中的例子的简化版,来直观的展示使用AbstractQueuedSynchronizer来实现同步组件的方便,如下所示。

public class MyLock {

    private final Sync sync = new Sync();

    public void lock() {
        sync.acquire(1);
    }

    public void unlock() {
        sync.release(1);
    }

    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int unused) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int unused) {
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
    }

}

上述的MyLock是一个提供了最简单功能的不可重入锁,该锁的Lock()unlock()方法全部是调用的AbstractQueuedSynchronizer提供的模板方法acquire()release(),然后在acquire()release()这两个模板方法中又会调用Sync重写的tryAcquire()tryRelease()方法,最终的效果就是仅仅重写了tryAcquire()tryRelease()方法,便实现了一个具有最简单功能的不可重入锁,而同步的具体实现细节比如同步队列等全都由AbstractQueuedSynchronizer来完成,借助AbstractQueuedSynchronizer,可以极大降低实现一个同步组件的门槛。

二. AbstractQueuedSynchronizer的原理

AbstractQueuedSynchronizer中,维护了一个FIFO的同步队列,当某个线程获取同步状态失败时,该线程会被封装成一个节点Node并加入同步队列中。本小节将结合同步队列和AbstractQueuedSynchronizer源码对AbstractQueuedSynchronizer的原理进行学习。

AbstractQueuedSynchronizer中有两个字段headtail,分别指向同步队列头节点和尾节点,同步队列中的每个节点也有两个字段prevnext,分别指向上一节点和下一节。一个同步队列的基本结构如下所示。

多线程学习-队列同步器_第1张图片

当有新节点入队列时,新节点会加入到队列尾并成为新的尾节点,同时tail字段会指向新的尾节点。节点入队列如下所示。

多线程学习-队列同步器_第2张图片

通常,head字段指向的头节点表示当前成功获取同步状态的线程所对应的节点,头节点释放同步状态后,会唤醒头节点的下一节点,当下一节点成功获取同步状态后,会将head字段指向自己从而成为新的头节点,与此同时,新头节点的prev和老头节点的next字段会被置为null,以帮助垃圾回收。头节点释放同步状态如下所示。

多线程学习-队列同步器_第3张图片

上述为同步队列的基本概览,下面将结合AbstractQueuedSynchronizer所提供的模板方法来对独占式获取释放同步状态共享式获取释放同步状态进行分析。

1. 独占式获取同步状态

独占式获取同步状态的acquire()方法如下所示。

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

acquire()方法做了如下几件事情:

  • 先调用重写的tryAcquire()方法独占式地获取同步状态,获取成功则返回,获取失败则执行下一步骤;
  • 调用addWaiter()方法基于当前线程创建一个节点Node并添加到同步队列中;
  • 调用acquireQueued()方法,使刚创建并加入了同步队列的Node进入自旋状态,在自旋状态中,Node会判断自己的上一节点是否是头节点,如果是则尝试获取同步状态,获取成功则退出自旋状态,如果自己的上一节点不是头节点或者获取同步状态失败,则阻塞自己,直到被上一节点唤醒或者被中断,此时重复前面的自旋过程。

还是从源码入手,分析创建节点Node,入同步队列和自旋判断并获取同步状态这几个过程。首先是addWaiter()方法,如下所示。

private Node addWaiter(Node mode) {
    //基于当前线程创建一个节点
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        //先尝试安全地将新创建的节点添加到同步队列尾
        //如果添加成功,则直接返回新创建的节点
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //如果新创建的节点是入同步队列的第一个节点,或者尝试添加失败
    //则调用enq()方法将节点安全地添加到同步队列中
    enq(node);
    return node;
}

addWaiter()中,基于当前线程创建节点Node之后,会立即尝试一次安全地将新创建的Node添加到队列尾,若失败的话则会调用enq()方法来让节点入队列,enq()的实现如下所示。

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) {
            //node如果是入同步队列的第一个节点,此时t为null
            //则先将head安全的指向一个空节点,然后head再赋值给tail
            //此时头节点和尾节点均为同一个空节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //尾节点不为null时,则尝试安全的将node设置为新的尾节点
            //如果设置失败,则循环重复尝试设置,直到设置成功为止
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

enq()方法实际就是在一个死循环中重复地基于CAS方式将新创建的节点入同步队列。成功入队列后的节点会被传入acquireQueued()方法进入自旋状态,其实现如下所示。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            //p为当前节点的上一节点
            final Node p = node.predecessor();
            //判断当前节点的上一节点是否是头节点
            //如果是则尝试获取同步状态
            if (p == head && tryAcquire(arg)) {
                //获取同步状态成功,则将当前节点置为头节点
                setHead(node);
                p.next = null;
                failed = false;
                return interrupted;
            }
            //先判断当前节点是否应该被阻塞
            //如果应该被阻塞则调用parkAndCheckInterrupt()方法进入阻塞状态
            //否则自旋重新进行上述步骤
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

acquireQueued()方法中,如果当前节点的上一节点不是头节点或者尝试获取同步状态失败,那么会在shouldParkAfterFailedAcquire()方法中判断当前节点的上一节点状态是否为SIGNAL,如果是则会调用parkAndCheckInterrupt()进入阻塞状态,如果不是则会将当前节点的上一节点状态置为SIGNAL,然后自旋地再执行上述步骤。

独占式获取同步状态还有两个模板方法,分别为acquireInterruptibly()tryAcquireNanos(),其中acquireInterruptibly()可以响应中断,tryAcquireNanos()可以指定等待时间,下面将对这两个方法进行简要分析。

acquireInterruptibly()的实现如下所示。

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

acquireInterruptibly()方法中尝试获取同步状态失败时,会调用doAcquireInterruptibly()方法来将当前线程加入同步队列,如下所示。

private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    //基于当前线程创建节点并加入同步队列尾
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null;
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                //等待过程中被中断则抛出中断异常
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

doAcquireInterruptibly()的实现基本与acquireQueued()相同,区别在于,在doAcquireInterruptibly()方法中进入等待状态时被中断的话,会抛出中断异常。

接下来再看一下tryAcquireNanos()方法的实现,如下所示。

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}

tryAcquireNanos()方法中获取同步状态失败时,会调用doAcquireNanos()方法来将当前线程加入同步队列,如下所示。

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    //计算等待的最晚时间点
    final long deadline = System.nanoTime() + nanosTimeout;
    //基于当前线程创建节点并加入同步队列尾
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null;
                failed = false;
                return true;
            }
            //计算剩余等待时间
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
                return false;
            //如果当前线程应该被阻塞,并且剩余等待时间大于1000纳秒,则使用LockSupport进入等待状态
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                //如果当前线程是因为被中断而从等待状态返回,则抛出中断异常
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

doAcquireNanos()方法的大体实现与acquireQueued()相同,不同之处在于首先doAcquireNanos()至多只会在同步队列中阻塞nanosTimeout的时间,超时的话会返回false表示等待获取同步状态失败,其次doAcquireNanos()也是响应中断的。

2. 独占式释放同步状态

AbstractQueuedSynchronizer提供了模板方法release()来实现独占式释放同步状态,如下所示。

public final boolean release(int arg) {
    //尝试释放同步状态
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            //释放同步状态之后,唤醒头节点的下一节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}

当前节点独占式地释放同步状态之后,会调用unparkSuccessor()方法来唤醒下一节点,如下所示。

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    //如果下一节点为null,或者下一节点状态为关闭,则从尾节点开始向前寻找满足唤醒条件的节点并唤醒
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

unparkSuccessor()方法会获取当前节点的下一节点,并判断下一节点是否为null或者状态是否为CANCELLED,如果符合其中任何一项,表明当前节点的下一节点不满足唤醒条件,那么需要从尾节点开始向前寻找满足唤醒条件的节点并唤醒,而唤醒条件就是节点不为null以及节点状态不为CANCELLED

3. 共享式获取同步状态

共享式获取同步状态的模板方法acquireShared()如下所示。

public final void acquireShared(int arg) {
    //获取同步状态失败则返回负值
    if (tryAcquireShared(arg) < 0)
        //创建节点加入同步队列尾,并进入自旋状态
        doAcquireShared(arg);
}

tryAcquireShared()方法对其返回值进行了如下规定。

  • 返回负值,表示共享式获取同步状态失败;
  • 返回正值,表示共享式获取同步状态成功,并且后续共享式获取同步状态还能成功;
  • 返回0,表示共享式获取同步状态成功,但后续共享式获取同步状态会失败。

因此如果通过tryAcquireShared()方法共享式获取同步状态失败,则会调用doAcquireShared()方法基于当前线程创建节点添加到同步队列尾,并进入自旋状态,tryAcquireShared()方法如下所示。

private void doAcquireShared(int arg) {
    //基于当前线程和Node.SHARED创建一个节点Node,并加入同步队列
    //Node.SHARED实际上是一个空Node对象,用于指示当前创建的节点是在共享模式下进行等待
    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);
                //tryAcquireShared()方法返回非负值,表示获取同步状态成功
                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);
    }
}

doAcquireShared()方法中,首先会基于当前线程创建一个节点Node,并会设置节点的nextWaiter字段为Node.SHARED,其中nextWaiter是用于指示当前节点是独占模式下等待的节点还是共享模式下等待的节点。创建好节点并添加到同步队列尾之后,节点就会进入自旋状态,整个自旋流程与独占式获取同步状态大体一致,而区别就在于自旋状态中节点成功获取到了同步状态之后的操作,这里如果在自旋状态中成功获取到了同步状态,那么就会对获取到同步状态的节点的下一节点进行判断,如果判断得到下一节点是在共享模式下等待的节点,则需要将其唤醒,整个判断是在setHeadAndPropagate()方法中实现的,如下所示。

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head;
    setHead(node);

    //propagate为tryAcquireShared()方法的返回值
    //propagate为负值表示共享式获取同步状态失败
    //propagate为0表示共享式获取同步状态成功,但是同步状态后续无法再被获取
    //propagate为正值表示共享式获取同步状态失败,同步状态后续还可以被获取
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        //如果当前节点的下一节点是共享模式下等待的节点,则将其唤醒
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

setHeadAndPropagate()方法中会先将共享式获取到同步状态的节点置为头节点,然后如果同步状态后续还可以被获取并且节点的下一节点是在共享模式下等待的节点,那么调用doReleaseShared()方法来唤醒共享模式下等待的节点,如下所示。

private void doReleaseShared() {
    for (;;) {
        //h表示本次循环执行时的头节点
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                //如果头节点状态为SIGNAL,表示头节点的下一节点处于等待状态
                //则先将头节点状态置为0,再将下一节点唤醒
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;
        }
        if (h == head)
            break;
    }
}

doReleaseShared()方法会每次获取循环执行时的头节点,因为节点状态为SIGNAL时节点的下一节点处于等待状态,所以如果头节点状态为SIGNAL则会先置头节点状态为0然后唤醒下一节点,下一节点被唤醒后进行自旋时如果共享式地获取到了同步状态,那么又会执行一轮上述的步骤,也就是说,共享模式下等待的节点的唤醒,会发生在同步状态释放后和上一节点获取到同步状态后,与之相比的独占模式下等待的节点的唤醒,只会发生在同步状态释放后。

上面的整个流程,以一个简单的例子加以说明,下图为一个同步队列的节点示意图。

Node1在自旋过程中判断上一节点是头节点,并且获取同步状态成功,此时Node1成为头节点,并判断同步状态还能继续获取以及下一节点Node2是在共享模式下等待的节点,所以Node2被唤醒,Node2被唤醒后进入自旋状态,并判断上一节点Node1是头节点,然后获取同步状态并成功,此时Node2成为头节点,因此,在同步状态可以继续获取以及下一节点是共享模式下等待的节点的情况下,共享模式下等待的节点均会被唤醒,直到同步状态无法再被获取或者下一节点是独占模式下等待的节点。

共享式获取同步状态还有两个方法acquireSharedInterruptibly()tryAcquireSharedNanos(),相较于acquireShared()方法分别增加了响应中断和指定等待时间的功能,除此之外这两个方法的实现与acquireShared()一致,故这里不再讨论这两个方法了。

4. 共享式释放同步状态

共享式释放同步状态的模板方法releaseShared()如下所示。

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

即调用重写的tryReleaseShared()方法共享式地释放同步状态成功之后,会调用doReleaseShared()方法,而前面已经知道doReleaseShared()方法会每次获取循环执行时的头节点,然后唤醒头节点的下一节点,如果下一节点是共享模式下等待的节点,那么这个唤醒动作会被传递下去,如果下一节点是独占模式下等待的节点,则唤醒的动作只会作用于这个独占模式下等待的节点。

总结

AbstractQueuedSynchronizer提供了用于构建锁或者同步组件的基础框架,通常使用方式为创建一个AbstractQueuedSynchronizer的子类并作为同步组件的静态内部类,然后重写其规定的可重写方法,然后同步组件功能的实现需要通过调用AbstractQueuedSynchronizer提供的模板方法,这些模板方法会调用其规定的可重写方法,最终可以按照我们的预期来实现同步功能。AbstractQueuedSynchronizer提供的模板方法,主要分为独占式获取同步状态独占式释放同步状态共享式获取同步状态共享式释放同步状态,这些模板方法为我们提供了具体的同步实现细节比如等待和唤醒,我们仅需将其规定的可重写方法重写好,便能可靠地实现一个自定义同步组件。

你可能感兴趣的:(多线程学习-队列同步器)