源码阅读之AbstractQueuedSynchronizer

15854876-4ea68b4164dba1db.png
AQS独占模式流程图.png
15854876-5a729b0ee8fe71b5.png
AQS共享模式获取同步状态流程图.png

所谓“锁”,在AQS中,其实质是让线程获取AQS中的state的状态值,一旦可以成功获取到状态值(读到值,并且将原值加1成功),那么该线程就有运行临界区代码的权限。

AQS仅仅只是提供独占锁和共享锁两种方式,但是每种方式都有响应中断和不响应中断的区别,所以AQS锁的更细粒度的划分为:
不响应中断的独占锁(acquire)
响应中断的独占锁(acquireInterruptibly)
不响应中断的共享锁(acquireShared)
响应中断的共享锁(acquireSharedInterruptibly)
这四种方式在AQS中的入口在上面已经标注,而释放锁的方式只有两种,独占锁的释放与共享锁的释放。分别为:
独占锁的释放(release)
共享锁的释放(releaseShared)
定义了一个内部类Node
内部类的几个属性

static final Node SHARED = new Node();  //标识是共享模式
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;  //标识独占模式
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED =  1;//等待状态值,表示线程已取消
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL    = -1;//等待状态值,表示继任的线程需要取消阻塞
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;//等待状态值,表示阻塞在某个condition下
/**
 * waitStatus value to indicate the next acquireShared should
 * unconditionally propagate
 */
static final int PROPAGATE = -3;//等待状态值,共享模式下,下一次获取需要无条件的进行传播(暂时不理解什么意思)

SIGNAL:     The successor of this node is (or will soon be)
*               blocked (via park), so the current node must
*               unpark its successor when it releases or
*               cancels. To avoid races, acquire methods must
*               first indicate they need a signal,
*               then retry the atomic acquire, and then,
*               on failure, block.

翻译:当当前节点的后续节点已经(或者很快将要)通过park进行了阻塞,那么当当前节点释放或者取消的时候必须unpark唤醒其后续节点,同时为了避免竞争,acquire方法必须只是需要signal信号,之后进行原子的重试acquire,失败的话,则进行阻塞。

CANCELLED:  This node is cancelled due to timeout or interrupt.
*               Nodes never leave this state. In particular,
*               a thread with cancelled node never again blocks.

由于超时或者中断处于取消状态,处于取消状态下的线程不能再次阻塞。

CONDITION:  This node is currently on a condition queue.
*               It will not be used as a sync queue node
*               until transferred, at which time the status
*               will be set to 0. (Use of this value here has
*               nothing to do with the other uses of the
*               field, but simplifies mechanics.)

当前节点处于等待队列中,知道状态被设置为0前它都不会被当做同步队列中的节点使用。

PROPAGATE:  A releaseShared should be propagated to other
*               nodes. This is set (for head node only) in
*               doReleaseShared to ensure propagation
*               continues, even if other operations have
*               since intervened.

acquire(int arg)方法

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

在类的解释中我们可以看到

*
 * <pre>
 * Acquire:
 *     while (!tryAcquire(arg)) {
 *        <em>enqueue thread if it is not already queued</em>;
           (如果线程尚未排队,则将其排队)
 *        <em>possibly block current thread</em>;
             (可能阻止当前线程)
 *     }
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;
}

这里的enq就是将其排队,只会在pred==null的时候进入一次

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;
            }
        }
    }
}

避免羊群效应,只会唤醒后续节点

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;//快速失败
    try {
        boolean interrupted = false;//默认不中断
        for (;;) {
            final Node p = node.predecessor();//获取当前节点的前驱节点
            //如果前一个节点是头节点,那么可能很快就轮到我了,要努力一下,try起来
            if (p == head && tryAcquire(arg)) {
                //try成功的话
                setHead(node);//将当前节点设置为头节点
                p.next = null; // help GC //之前的头节点你可以消失了
                failed = false;
                return interrupted;
            }
            //看下是否应该进行阻塞,不做无味的努力
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
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.
         * 可以安心睡了,后续SIGNAL的节点会来唤醒它的
         */
        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;
}
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

当前线程就会被阻塞,

Thread.interrupted()会清除当前中断位,之后设置为true,下一次for循环并且成功获取的情况下可以表示是被唤醒的。
/**
 * Cancels an ongoing attempt to acquire.
 *
 * @param node the node
 */
private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    if (node == null)
        return;

    node.thread = null;

    // Skip cancelled predecessors
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    // predNext is the apparent node to unsplice. CASes below will
    // fail if not, in which case, we lost race vs another cancel
    // or signal, so no further action is necessary.
    Node predNext = pred.next;

    // Can use unconditional write instead of CAS here.
    // After this atomic step, other Nodes can skip past us.
    // Before, we are free of interference from other threads.
    node.waitStatus = Node.CANCELLED;

    // If we are the tail, remove ourselves.
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        // If successor needs signal, try to set pred's next-link
        // so it will get one. Otherwise wake it up to propagate.
        int ws;
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            unparkSuccessor(node);
        }

        node.next = node; // help GC
    }
}
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. 首先拿到head节点,判断head节点不等于null,并且head节点的waitStatus是不等于0的话,就去唤醒head节点的下一个节点所持有的线程。
  2. 调用unparkSuccessor(Node node)方法唤醒head节点的下一个节点所持有的线程。
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    Node s = node.next;
    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);
}

用unpark()唤醒等待队列中最前边的那个未放弃的线程s。此时,再和acquireQueued()联系起来,s被唤醒后,进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然s已经是等待队列中最前边的那个未放弃的线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次自旋p==head就成立啦),然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也会返回

我们常用的

ReentrantLock reentrantLock = new ReentrantLock();
CountDownLatch countDownLatch = new CountDownLatch();

等的底层都是AbstractQueuedSynchronizer
我们看ReentrantLock的源码如下

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;

    /**
     * Base of synchronization control for this lock. Subclassed
     * into fair and nonfair versions below. Uses AQS state to
     * represent the number of holds on the lock.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {

我们直接看AbstractQueuedSynchronizer的源码

/**
 * Provides a framework for implementing blocking locks and related
 * synchronizers (semaphores, events, etc) that rely on
 * first-in-first-out (FIFO) wait queues. 
 (提供一个框架,用于实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量,事件等))
 
 * This class is designed to
 * be a useful basis for most kinds of synchronizers that rely on a
 * single atomic {@code int} value to represent state. 
 (这个类被设计为大部分同步器的基础,它依赖于单个原子值来表示状态)
 
 * Subclasses
 * must define the protected methods that change this state, and which
 * define what that state means in terms of this object being acquired
 * or released.  
 (子类可以重写protected方法来改变这个状态值,并且通过该状态的值来定义是获取还是释放)
 
 * Given these, the other methods in this class carry
 * out all queuing and blocking mechanics. 
 (基于此,其他方法可以实现所有的阻塞和队列机制)
 
 * Subclasses can maintain
 * other state fields, but only the atomically updated {@code int}
 * value manipulated using methods {@link #getState}, {@link
 * #setState} and {@link #compareAndSetState} is tracked with respect
 * to synchronization.
 (子类可以维护其他状态字段,但基本操作都是getState,setState,compareAndSetState)
 
 * <p>Subclasses should be defined as non-public internal helper
 * classes that are used to implement the synchronization properties
 * of their enclosing class.  
 (子类应定义为非公共内部辅助类,用于实现其内部类的同步属性。)
 
 * Class
 * {@code AbstractQueuedSynchronizer} does not implement any
 * synchronization interface.  
 (AbstractQueuedSynchronizer没有实现任何同步接口)
 
 * Instead it defines methods such as
 * {@link #acquireInterruptibly} that can be invoked as
 * appropriate by concrete locks and related synchronizers to
 * implement their public methods.
 (相反,它定义了{@link #acquireInterruptibly}等方法,可以通过具体的锁和相关的同步器来适当调用它们来实现它们的公共方法。)
 
 * <p>This class supports either or both a default <em>exclusive</em>
 * mode and a <em>shared</em> mode.
 (这个类即提供了默认的独占模型,也提供了共享模型)
 
 * When acquired in exclusive mode,
 * attempted acquires by other threads cannot succeed. 
 (当使用独占模型去获取时,其他线程就不能再获取了)
 
 * Shared mode
 * acquires by multiple threads may (but need not) succeed. 
 (共享模型下,多个线程可以同时获取成功)
 
 * This class
 * does not &quot;understand&quot; 
 (翻译不了)
 
 * these differences except in the
 * mechanical sense that when a shared mode acquire succeeds, the next
 * waiting thread (if one exists) must also determine whether it can
 * acquire as well. 
 (这些差异主要在当共享模式下获取成功的,下一个等待的线程依然可以决定是否去获取)
 
 * same FIFO queue. Usually, implementation subclasses support only
 * one of these modes, but both can come into play for example in a
 * {@link ReadWriteLock}. 
 (同样的FIFO队列,子类通常只会实现这两种模式中的一种,但也可以两种都实现,比如像ReadWriteLock)
 
 * Subclasses that support only exclusive or
 * only shared modes need not define the methods supporting the unused mode.
 (仅支持独占模式或仅支持共享模式的子类无需定义支持未使用模式的方法。)
 
 *
 * <p>This class defines a nested {@link ConditionObject} class that
 * can be used as a {@link Condition} implementation by subclasses
 * supporting exclusive mode for which method {@link
 * #isHeldExclusively} reports whether synchronization is exclusively
 * held with respect to the current thread, 
 (该类定义了一个嵌套的{@link ConditionObject}类,可以通过支持独占模式的子类用作{@link Condition}实现,方法{@link #isHeldExclusively}是否被该线程独占)
 
 * invoked with the current {@link #getState} value fully releases
 * this object, and {@link #acquire}, given this saved state value,
 * eventually restores this object to its previous acquired state. 
 (使用当前的{@link #getState}值调用完全释放此对象,acquire 获取已经保存的状态值,
 最终将此对象恢复到之前获得的状态)
 
 
 * No
 * {@code AbstractQueuedSynchronizer} method otherwise creates such a
 * condition, so if this constraint cannot be met, do not use it.  The
 * behavior of {@link ConditionObject} depends of course on the
 * semantics of its synchronizer implementation.
 
 (AbstractQueuedSynchronizer的方法满足不了的时候,你可以创建一个condition,如果不是这样,尽量不要使用它,{@link ConditionObject}的行为当然取决于其同步器实现的语义)
 
 
 * <p>This class provides inspection, instrumentation, and monitoring
 * methods for the internal queue, as well as similar methods for
 * condition objects. 
 (该类也提供了对内部队列的检查,检测,监控的方法,对condition 对象也一样)
 
 * These can be exported as desired into classes
 * using an {@code AbstractQueuedSynchronizer} for their
 * synchronization mechanics.
 (这些可以根据需要使用{@code AbstractQueuedSynchronizer}导出到类中,用于它们的同步机制。)
 
 *
 * <p>Serialization of this class stores only the underlying atomic
 * integer maintaining state, so deserialized objects have empty
 * thread queues.
 (此类只保证了原子状态的序列化,反序列化时会得到一个空的线程队列)
 
 * Typical subclasses requiring serializability will
 * define a {@code readObject} method that restores this to a known
 * initial state upon deserialization.
 (如果子类需要序列化,可以实现readObject方法,保证在反序列化时将其恢复到已知的初始状态)
 
 * <h3>Usage</h3>
 *
 * <p>To use this class as the basis of a synchronizer, redefine the
 * following methods, as applicable, by inspecting and/or modifying
 * the synchronization state using {@link #getState}, {@link
 * #setState} and/or {@link #compareAndSetState}:
 *
 * <ul>
 * <li> {@link #tryAcquire}
 * <li> {@link #tryRelease}
 * <li> {@link #tryAcquireShared}
 * <li> {@link #tryReleaseShared}
 * <li> {@link #isHeldExclusively}
 * </ul>
 (通过重写这几个方法来实现我们自己的逻辑)
 
 * Each of these methods by default throws {@link
 * UnsupportedOperationException}.  
 
 * Implementations of these methods
 * must be internally thread-safe, and should in general be short and
 * not block. 
 (这些方法的实现必须是内部线程安全的,并且不应该有阻塞)
 
 * Defining these methods is the <em>only</em> supported
 * means of using this class. All other methods are declared
 * {@code final} because they cannot be independently varied.
 (其他方法都是final)
 
 * <p>You may also find the inherited methods from {@link
 * AbstractOwnableSynchronizer} useful to keep track of the thread
 * owning an exclusive synchronizer.  
 (您也可以从{@link AbstractOwnableSynchronizer}中找到继承的方法,以便跟踪拥有独占同步器的线程)
 
 * You are encouraged to use them
 * -- this enables monitoring and diagnostic tools to assist users in
 * determining which threads hold locks.
 (建议您使用它们,这使监视和诊断工具能够帮助用户确定哪些线程持有锁。)

 * <p>Even though this class is based on an internal FIFO queue, it
 * does not automatically enforce FIFO acquisition policies.  
 (即使此类基于内部FIFO队列,它也不会自动执行FIFO获取策略。)
 
 * The core
 * of exclusive synchronization takes the form:
 (独占同步的核心就是这种形式)
 
 *
 * <pre>
 * Acquire:
 *     while (!tryAcquire(arg)) {
 *        <em>enqueue thread if it is not already queued</em>;
           (如果线程尚未排队,则将其排队)
 *        <em>possibly block current thread</em>;
             (可能阻止当前线程)
 *     }
 *
 * Release:
 *     if (tryRelease(arg))
 *        <em>unblock the first queued thread</em>;
             (取消队列第一个线程的阻塞)
 * </pre>

 * (Shared mode is similar but may involve cascading signals.)
 (共享模式类似,但可能涉及级联唤醒。)

 * <p id="barging">Because checks in acquire are invoked before
 * enqueuing, a newly acquiring thread may <em>barge</em> ahead of
 * others that are blocked and queued.  
 (入队之前,需要先检查获取操作,新进入的线程可能会先于阻塞队列和等待队列里的其他线程获取到资源)
 
 * However, you can, if desired,
 * define {@code tryAcquire} and/or {@code tryAcquireShared} to
 * disable barging by internally invoking one or more of the inspection
 * methods,
 (大概意思就是如果你愿意,你也可以自定义tryAcquire方法通过内部调用一个或多个检查方法来禁用插入不这么做。)
 
 * thereby providing a <em>fair</em> FIFO acquisition order.
 (从而提供公平的FIFO获取顺序。)
 
 * In particular, most fair synchronizers can define {@code tryAcquire}
 * to return {@code false} if {@link #hasQueuedPredecessors} (a method
 * specifically designed to be used by fair synchronizers) returns
 * {@code true}.  Other variations are possible.
 *
 * <p>Throughput and scalability are generally highest for the
 * default barging (also known as <em>greedy</em>,
 * <em>renouncement</em>, and <em>convoy-avoidance</em>) strategy.
 * While this is not guaranteed to be fair or starvation-free, 
 (非公平可以保证很高的吞吐量,但不能保证公平和无饥饿)
 
 * earlier
 * queued threads are allowed to recontend before later queued
 * threads, and each recontention has an unbiased chance to succeed
 * against incoming threads.  
 (允许先前排队的线程在稍后排队的线程之前重新调度,并且重新进入的线程有一个公平的机会,就是大家都公平了)
 
 * Also, while acquires do not
 * &quot;spin&quot; 
 (获取也不会进行自旋)
 
 * in the usual sense, they may perform multiple
 * invocations of {@code tryAcquire} interspersed with other
 * computations before blocking. 
 (通常,tryAcquire在阻塞之前会进行多次调用)
 
 * This gives most of the benefits of
 * spins when exclusive synchronization is only briefly held, without
 * most of the liabilities when it isn't. 
 (线程切换很频繁时,自旋有很大的好处)
 
 * If so desired, you can
 * augment this by preceding calls to acquire methods with
 * "fast-path" checks, possibly prechecking {@link #hasContended}
 * and/or {@link #hasQueuedThreads} to only do so if the synchronizer
 * is likely not to be contended.
 (可以通过先前的方法来预估这次的自旋时间)
 
 *
 * <p>This class provides an efficient and scalable basis for
 * synchronization in part by specializing its range of use to
 * synchronizers that can rely on {@code int} state, acquire, and
 * release parameters, and an internal FIFO wait queue. When this does
 * not suffice, you can build synchronizers from a lower level using
 * {@link java.util.concurrent.atomic atomic} classes, your own custom
 * {@link java.util.Queue} classes, and {@link LockSupport} blocking
 * support.
 *
 * <h3>Usage Examples</h3>
 *
 * <p>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:
 *
 *  <pre> {@code
 * class Mutex implements Lock, java.io.Serializable {
 *
 *   // Our internal helper class
 *   private static class Sync extends AbstractQueuedSynchronizer {
 *     // Reports whether in locked state
 *     protected boolean isHeldExclusively() {
 *       return getState() == 1;
 *     }
 *
 *     // Acquires the lock if state is zero
 *     public boolean tryAcquire(int acquires) {
 *       assert acquires == 1; // Otherwise unused
 *       if (compareAndSetState(0, 1)) {
 *         setExclusiveOwnerThread(Thread.currentThread());
 *         return true;
 *       }
 *       return false;
 *     }
 *
 *     // Releases the lock by setting state to zero
 *     protected boolean tryRelease(int releases) {
 *       assert releases == 1; // Otherwise unused
 *       if (getState() == 0) throw new IllegalMonitorStateException();
 *       setExclusiveOwnerThread(null);
 *       setState(0);
 *       return true;
 *     }
 *
 *     // Provides a Condition
 *     Condition newCondition() { return new ConditionObject(); }
 *
 *     // Deserializes properly
 *     private void readObject(ObjectInputStream s)
 *         throws IOException, ClassNotFoundException {
 *       s.defaultReadObject();
 *       setState(0); // reset to unlocked state
 *     }
 *   }
 *
 *   // The sync object does all the hard work. We just forward to it.
 *   private final Sync sync = new Sync();
 *
 *   public void lock()                { sync.acquire(1); }
 *   public boolean tryLock()          { return sync.tryAcquire(1); }
 *   public void unlock()              { sync.release(1); }
 *   public Condition newCondition()   { return sync.newCondition(); }
 *   public boolean isLocked()         { return sync.isHeldExclusively(); }
 *   public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
 *   public void lockInterruptibly() throws InterruptedException {
 *     sync.acquireInterruptibly(1);
 *   }
 *   public boolean tryLock(long timeout, TimeUnit unit)
 *       throws InterruptedException {
 *     return sync.tryAcquireNanos(1, unit.toNanos(timeout));
 *   }
 * }}</pre>
 *
 * <p>Here is a latch class that is like a
 * {@link java.util.concurrent.CountDownLatch CountDownLatch}
 * except that it only requires a single {@code signal} to
 * fire. Because a latch is non-exclusive, it uses the {@code shared}
 * acquire and release methods.
 (栅栏,只需要一个信号来触发,比如CountDownLatch)
 
 *
 *  <pre> {@code
 * class BooleanLatch {
 *
 *   private static class Sync extends AbstractQueuedSynchronizer {
 *     boolean isSignalled() { return getState() != 0; }
 *
 *     protected int tryAcquireShared(int ignore) {
 *       return isSignalled() ? 1 : -1;
 *     }
 *
 *     protected boolean tryReleaseShared(int ignore) {
 *       setState(1);
 *       return true;
 *     }
 *   }
 *
 *   private final Sync sync = new Sync();
 *   public boolean isSignalled() { return sync.isSignalled(); }
 *   public void signal()         { sync.releaseShared(1); }
 *   public void await() throws InterruptedException {
 *     sync.acquireSharedInterruptibly(1);
 *   }
 * }}</pre>
 *
 * @since 1.5
 * @author Doug Lea
 */

猜你喜欢

转载自blog.csdn.net/weixin_34168880/article/details/87550349