逐渐深入Java多线程(三)----BlockingQueue阻塞队列及其实现类简介

目录

BlockingQueue简介

BlockingQueue的实现类

1,ArrayBlockingQueue

2,DelayQueue

3,LinkedBlockingDeque

4,LinkedBlockingQueue

5,LinkedTransferQueue

6,PriorityBlockingQueue

7,SynchronousQueue


BlockingQueue简介

java.util.concurrent. BlockingQueue接口自JDK1.5加入java,意思是阻塞队列,不过接口里面的方法也不都是阻塞的,BlockingQueue提供了对阻塞队列的多种处理方案。

 

BlockingQueue对于元素的添加,删除,检查等操作,分别用不同的方法提供了不同的处理方案,比如抛出异常,返回特殊值,阻塞,超时等。

BlockingQueue类的注释中提供了这样一份方法使用总结:

* <table BORDER CELLPADDING=3 CELLSPACING=1>
* <caption>Summary of BlockingQueue methods</caption>
*  <tr>
*    <td></td>
*    <td ALIGN=CENTER><em>Throws exception</em></td>
*    <td ALIGN=CENTER><em>Special value</em></td>
*    <td ALIGN=CENTER><em>Blocks</em></td>
*    <td ALIGN=CENTER><em>Times out</em></td>
*  </tr>
*  <tr>
*    <td><b>Insert</b></td>
*    <td>{@link #add add(e)}</td>
*    <td>{@link #offer offer(e)}</td>
*    <td>{@link #put put(e)}</td>
*    <td>{@link #offer(Object, long, TimeUnit) offer(e, time, unit)}</td>
*  </tr>
*  <tr>
*    <td><b>Remove</b></td>
*    <td>{@link #remove remove()}</td>
*    <td>{@link #poll poll()}</td>
*    <td>{@link #take take()}</td>
*    <td>{@link #poll(long, TimeUnit) poll(time, unit)}</td>
*  </tr>
*  <tr>
*    <td><b>Examine</b></td>
*    <td>{@link #element element()}</td>
*    <td>{@link #peek peek()}</td>
*    <td><em>not applicable</em></td>
*    <td><em>not applicable</em></td>
*  </tr>
* </table>

这段html的代码展示出来是这样的:

翻译一下是这样的:

注:

1,向固定长度的队列添加元素时,官方建议用offer()方法,比add()强。

2,element()方法和peek()方法是父接口Queue中定义的,在BlockingQueue中没有显式定义,他们的功能是检查队列的头元素,但不会删除头元素(这是这俩方法和offer()等方法的区别)。

 

另外,BlockingQueue还提供了下面的方法:

1,int remainingCapacity();

检查队列中的剩余空间,也就是还能添加的元素数。不过,不能依靠这个方法来判断能不能往队列添加元素,因为可能有多个线程正在试图添加元素。

2,public boolean contains(Object o);

检查队列中是否包含某元素,用equals()方法来判断目标对象。

3,int drainTo(Collection<? super E> c);

从BlockingQueue中删除所有元素,并添加到参数集合中,返回添加成功的元素个数。比循环获取元素效率高一点。

4,int drainTo(Collection<? super E> c, int maxElements);

和上面的方法相比,多出来的maxElements参数代表最多转移的元素个数。

 

BlockingQueue的实现类

BlockingQueue的实现类默认有以下几种:

1,ArrayBlockingQueue

JDK1.5加入。

基于数组,队列长度固定,队列元素符合先进先出的原则。

关于公平性:

ArrayBlockingQueue有以下几种构造:

public ArrayBlockingQueue(int capacity) {
    this(capacity, false);
}

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}

public ArrayBlockingQueue(int capacity, boolean fair,
                          Collection<? extends E> c) {
    this(capacity, fair);

    final ReentrantLock lock = this.lock;
    lock.lock(); // Lock only for visibility, not mutual exclusion
    try {
        int i = 0;
        try {
            for (E e : c) {
                checkNotNull(e);
                items[i++] = e;
            }
        } catch (ArrayIndexOutOfBoundsException ex) {
            throw new IllegalArgumentException();
        }
        count = i;
        putIndex = (i == capacity) ? 0 : i;
    } finally {
        lock.unlock();
    }
}

其中的fair参数就是公平性的设置,默认为false。

ArrayBlockingQueue的很多方法使用ReentrantLock作为锁,比如put()方法,offer()方法等,如果公平性设置为true,则先申请锁的线程会先获得锁,也即是一种先进先出的策略。开启公平性会降低吞吐效率,好处是降低了变数,同时避免饥饿(有的线程总是拿不到锁)。

 

2,DelayQueue

JDK1.5加入

延迟队列,基于PriorityQueue(优先级队列),是一个建立在优先级基础上,并且元素有延迟时间的队列。

DelayQueue中只能放实现了Delayed接口的对象,这种对象一般需要实现两个方法:

long getDelay(TimeUnit unit);

int compareTo(T o);

分别是获得延迟剩余时间的方法和比较排序用的方法。

Delayed接口的代码:

public interface Delayed extends Comparable<Delayed> {

    /**
     * Returns the remaining delay associated with this object, in the
     * given time unit.
     *
     * @param unit the time unit
     * @return the remaining delay; zero or negative values indicate
     * that the delay has already elapsed
     */
    long getDelay(TimeUnit unit);
}

只有一个getDelay()方法,用于获得元素的剩余延迟时间,当延迟时间小于等于0时,标识延迟时间已过,可以被操作。

另外Delayed接口继承了Comparable接口:

public interface Comparable<T> {

    public int compareTo(T o);
}

也是只有一个接口,用于排序。

从上面两个接口可以看到,可以向DelayQueue中存放的元素都有延迟时间,而且放入队列中时会被排序,不一定是先进先出了。

 

下面以offer()方法为例,看看DelayQueue 新增元素时是怎么排序的:

public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        q.offer(e);
        if (q.peek() == e) {
            leader = null;
            available.signal();
        }
        return true;
    } finally {
        lock.unlock();
    }
}

除了使用锁之外,代码调用了变量q的offer()方法,变量q就是DelayQueue中维护的优先级队列:

private final PriorityQueue<E> q = new PriorityQueue<E>();

使用的是PriorityQueue的无参构造,PriorityQueue类本身维护了一个Comparator,无参构造中的Comparator是null。

PriorityQueue的offer()方法是这样的:

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e);
    return true;
}

可以看到当队列里没有元素的时候就直接把元素放在队列的第一个位置,否则调用siftUp()方法:

private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}

根据队列是否有comparator,来决定是用PriorityQueue的comparator还是用元素自己的compareTo()方法。根据上面的代码我们看到,DelayQueue中的PriorityQueue不维护comparator,所以我们看一下siftUpComparable()方法:

private void siftUpComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>) x;
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (key.compareTo((E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = key;
}

代码中使用了元素的compareTo()方法,用以排序。

 

下面以take()方法为例,看看DelayQueue 获得元素时是怎么考虑延迟时间的:

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        for (;;) {
            E first = q.peek();
            if (first == null)
                available.await();
            else {
                long delay = first.getDelay(NANOSECONDS);
                if (delay <= 0)
                    return q.poll();
                first = null; // don't retain ref while waiting
                if (leader != null)
                    available.await();
                else {
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        available.awaitNanos(delay);
                    } finally {
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        if (leader == null && q.peek() != null)
            available.signal();
        lock.unlock();
    }
}

可以看到,方法首先用peek()方法获取队列第一个元素,然后调用元素的getDelay()方法获得延迟剩余时间,如果剩余时间小于等于0,则调用poll()方法获取元素。

 

3,LinkedBlockingDeque

JDK1.6加入。

基于双向链表,可以指定队列长度,如果不指定队列长度则上限为Integer.MAX_VALUE。此队列的链接节点在每次插入时都会动态创建。

LinkedBlockingDeque和其他阻塞队列略有不同,他的链表是双向的,也就是链表两头都可进行出队或者入队,因为他实现的是BlockingDeque接口,BlockingDeque接口不但继承了BlockingQueue接口,还同时继承了Deque接口,Deque接口分别定义了头部和尾部的出入队方法。

如果我们在使用他的时候总是在队尾出队或入队,实际的效果就像一个栈一样。

 

简单介绍一下Deque接口,这个接口定义了一个双向列表,所谓deque,就是double ended queue的缩写,直译是双尾队列,实际上这个队列还是有明确的头和尾的,说双尾只是说在两头都能出入队,因此在这个接口中定义的方法都要指明是在队首操作还是在队尾操作。

Deque接口的注释给我们总结了相关方法的功能,也是在注释中写的html代码,翻译后是这样的:

 

下面介绍一下BlockingDeque接口,这个接口同时继承了BlockingQueue接口和Deque接口,而且在Deque接口的基础上扩展了几个方法,BlockingDeque接口的方法汇总如下:

理论上双向链表提供的功能是可以覆盖单向链表的,毕竟人家两头都能操作,BlockingDeque接口在注释中还贴心的介绍了BlockingQueue的方法在BlockingDeque中的等价方法:

其实也很好理解,入队走队尾,出队走队首,检查查队首,队列的设计本来就是这样的。

 

LinkedBlockingDeque还提供了其他几个方法:

removeFirstOccurrence(Object o)

removeLastOccurrence(Object o)

来自BlockingDeque接口。

作用是从队列中删除一个元素,这两个方法分别从队列头开始向后遍历,或者从队尾向前遍历,删除后返回true。

 

pop(),同removeFirst()

push(E e),同addFirst(e)

上面两个方法都来自Deque接口,在Deque接口中就同时存在这两个同义方法,不知道为什么要做这样重复的方法定义。

 

Iterator<E> iterator()

Iterator<E> descendingIterator()

来自Deque接口,分别提供了从队首到队尾,和从队尾到队首的迭代器。

 

Spliterator<E> spliterator()

JDK1.8加入,得到的是基于Spliterator的可分割迭代器。

 

4,LinkedBlockingQueue

JDK1.5加入。

基于链表,队列元素符合先进先出的原则。可以指定队列长度,如果不指定队列长度则上限为Integer.MAX_VALUE。

此队列的出队和入队用的锁是分开的。

线程池中的Executors. newFixedThreadPool() 使用了这个队列。

 

5,LinkedTransferQueue

JDK1.7加入。

基于链表,队列长度不限,操作基本不靠加锁,靠CAS来实现。

这是一个很有特点的队列,名字中的Transfer是转交的意思,这种转交和SynchronousQueue挺像,不过操作更丰富一点,在后面的介绍中可以加深对这个名字的理解。

LinkedTransferQueue的特点主要来源于两方面,一是队列节点的设计,二是出入队规则的设计,下面分别来看。

 

节点设计:

LinkedTransferQueue的节点维护了以下几个属性:

1,final boolean isData

节点类型。该队列的节点有两种类型:数据节点(isData==true)和请求节点(isData==false),数据节点是入队操作时添加的节点,这个操作和其他队列一样,而请求节点是出队操作时添加的节点,没错,LinkedTransferQueue的出队操作有可能会往队列中添加一个节点。

2,volatile Object item

队列中的元素。数据节点的item是非null的,请求节点的item是null。和其他队列不同之处在于,这个元素是有可能发生改变的,入队操作发现请求节点时从null变成非null,出队操作发现数据节点时从非null变成null,另外这种变化是CAS操作。

3,volatile Node next

节点的下一节点,和其他基于链表的队列基本相同。

4,volatile Thread waiter

等待线程。

 

出入队规则:

看完上面的节点设计大概就能猜到LinkedTransferQueue是怎么玩的了,而他的出入队规则大概描述一下就是这样的:

入队操作时,查找队列中第一个请求节点(由出队操作生成),把入队的item放入请求节点的item参数,然后唤醒请求节点的等待线程。如果此时队列中没有请求节点,则按照入队时的处理方案来处理。

出队操作时,查找队列中第一个数据节点(由入队操作生成),数据节点的item设为null,唤醒数据节点的等待线程(如果有)。如果此时队列中没有数据节点,则按照入队时的处理方案来处理。

总结一下就是:使用对称操作,出队入队都是查找与自己不同类型的节点,查到了就交换item,唤醒等待线程,查不到就按既定方案处理(既定方案在后面解释)。

 

LinkedTransferQueue的这种出入队规则是基于Dual Queue算法稍作修改而来,队列中既可以存放数据节点又可以存放请求节点的策略来自于此。

在Dual Queue算法的基础上,LinkedTransferQueue使用了一种松弛策略,因而匹配成功的节点不一定会立即出队,牺牲了一点遍历队列的效率,好处是减少了CAS操作的竞争开销。

 

其他队列在出队入队时提供的处理方案往往是:抛出异常,返回特定值,阻塞,和超时,而LinkedTransferQueue提供的处理方案有:

1,NOW。​​​​​​

立刻返回结果,不论成功失败,不会阻塞。此方案的入队方法只有在队列中包含请求节点时才会返回成功,出队方法也只有在队列中包含数据节点时才会返回成功。

poll()方法(出队),tryTransfer()方法(入队),用到了这个方案。

2,ASYNC

异步操作。由于此队列长度不限,所以异步入队基本上是必然成功的,而此队列的出队可以通过增加请求节点(占座)的方式完成,基本上也是必然成功的,所以此方案下的出入队基本都会成功。

offer()方法(出队),put()方法(入队),add()(入队)方法用到了这个方案。可以看到,除了LinkedTransferQueue特有的transfer()和tryTransfer()外,入队方法使用的都是这种方案。

3,SYNC

同步操作。阻塞线程直到成功为止。

transfer()方法(入队),take()方法(出队)用到了这个方案。

4,TIMED

超时方案。阻塞线程直到操作成功。

poll()方法,tryTransfer()方法(带超时时间的重载版)使用了这个方案。

 

类中对这几种处理方案有以下的常量定义:

/*
 * Possible values for "how" argument in xfer method.
 */
private static final int NOW   = 0; // for untimed poll, tryTransfer
private static final int ASYNC = 1; // for offer, put, add
private static final int SYNC  = 2; // for transfer, take
private static final int TIMED = 3; // for timed poll, tryTransfer

根据上面的内容,我们就可以理解LinkedTransferQueue中Transfer的含义。其转交的概念在transfer()和tryTransfer()这两个入队方法中得到了体现,这两个方法只有在队列中包含请求节点时才会操作成功,而在这种场景下,请求节点的线程会让请求节点立即出队,表现出的效果就是要入队的元素基本没在队列里呆过,入队线程把元素直接交给了出队线程,也就是所谓的转交。

 

和SynchronousQueue的区别:

LinkedTransferQueue和SynchronousQueue的套路非常像,都用了双向链表,SynchronousQueue是在线程链表中排队转交,而LinkedTransferQueue是在本队列中排队转交。

LinkedTransferQueue在Dual Queue的基础上用了松弛策略,匹配到的节点不一定立即出队,SynchronousQueue使用的就是Dual Queue策略,匹配到了就一起出队。

SynchronousQueue至少还用了一把锁,也用到了CAS操作,LinkedTransferQueue基本是靠CAS操作来完成的。

LinkedTransferQueue比SynchronousQueue更通用,也更强大,速度据说也更快。

LinkedTransferQueue提供立即返回结果的接口transfer()和tryTransfer(),不阻塞,不等超时。

 

另外值得指出的是,LinkedTransferQueue中的出队入队方法,包括独有的transfer()和tryTransfer(),代码实现上都调用了同一个方法:xfer(),因为此队列把出队和入队视为对称操作,核心方法代码合并成了一个,可以说是此队列的灵魂了:

private E xfer(E e, boolean haveData, int how, long nanos) {
    if (haveData && (e == null))
        throw new NullPointerException();
    Node s = null;                        // the node to append, if needed

    retry:
    for (;;) {                            // restart on append race

        for (Node h = head, p = h; p != null;) { // find & match first node
            boolean isData = p.isData;
            Object item = p.item;
            if (item != p && (item != null) == isData) { // unmatched
                if (isData == haveData)   // can't match
                    break;
                if (p.casItem(item, e)) { // match
                    for (Node q = p; q != h;) {
                        Node n = q.next;  // update by 2 unless singleton
                        if (head == h && casHead(h, n == null ? q : n)) {
                            h.forgetNext();
                            break;
                        }                 // advance and retry
                        if ((h = head)   == null ||
                            (q = h.next) == null || !q.isMatched())
                            break;        // unless slack < 2
                    }
                    LockSupport.unpark(p.waiter);
                    return LinkedTransferQueue.<E>cast(item);
                }
            }
            Node n = p.next;
            p = (p != n) ? n : (h = head); // Use head if p offlist
        }

        if (how != NOW) {                 // No matches available
            if (s == null)
                s = new Node(e, haveData);
            Node pred = tryAppend(s, haveData);
            if (pred == null)
                continue retry;           // lost race vs opposite mode
            if (how != ASYNC)
                return awaitMatch(s, pred, e, (how == TIMED), nanos);
        }
        return e; // not waiting
    }
}

方法的几个参数代表的含义是:

E e

要处理的元素节点。入队操作此参数为null。

boolean haveData

操作是否有数据,入队操作此参数为true,出队操作为false。

int how

处理方案,包括NOW,ASYNC,SYNC,TIMED。

long nanos

超时时间,处理方案是TIMED时会用到。

 

关于LinkedTransferQueue的更详细介绍,网上的这篇文章是非常好的:

https://segmentfault.com/a/1190000016460411

 

6,PriorityBlockingQueue

JDK1.5加入。

基于数组,默认容量是11。最大容量是Integer.MAX_VALUE – 8。

不允许null值。队列中的元素会按照指定排序方式排序。同优先级元素不保证顺序。

不可比较的元素无法加入此队列。

 

排序算法基于二叉堆,二叉堆的数据结构类似完全二叉树或近似完全二叉树。当我们用数组表示二叉堆时,数组中的节点有这样的特点:假设一个节点在数组中的位置是n,那么他在二叉堆中左孩子节点在数组中的位置是2*n+1,右孩子节点的位置是2*n+2,而他的父节点在数组中的位置是(n-1)>>1

 

二叉堆添加节点的逻辑:

1,在队尾添加节点。

2,节点和父节点比较,如果父节点比较自己小,则两个节点交换位置。

3,重复第二步,继续比较上级父节点,直到不能继续交换为止。

源码如下(无指定Compartor):

private static <T> void siftUpComparable(int k, T x, Object[] array) {
    Comparable<? super T> key = (Comparable<? super T>) x;
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = array[parent];
        if (key.compareTo((T) e) >= 0)
            break;
        array[k] = e;
        k = parent;
    }
    array[k] = key;
}

其中参数int k是要加入的节点位置,刚开始添加时肯定是队尾,参数T x是要添加的元素,参数Object[] array是二叉堆的数组。

 

二叉堆删除节点的逻辑:

1,从队首删除节点,队首出现空位。确认队尾节点。

2,用队尾节点比较空位的左右两个子节点,如果队尾节点小,队尾节点直接放入空位,删除节点逻辑结束,否则比较其左右两个子节点,小的子节点放入空位。

3,重复第二步直到队尾节点放入正确的空位。

源码如下(无指定Compartor):

private static <T> void siftDownComparable(int k, T x, Object[] array,
                                           int n) {
    if (n > 0) {
        Comparable<? super T> key = (Comparable<? super T>)x;
        int half = n >>> 1;           // loop while a non-leaf
        while (k < half) {
            int child = (k << 1) + 1; // assume left child is least
            Object c = array[child];
            int right = child + 1;
            if (right < n &&
                ((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
                c = array[child = right];
            if (key.compareTo((T) c) <= 0)
                break;
            array[k] = c;
            k = child;
        }
        array[k] = key;
    }
}

其中参数int k是要空节点下标,刚开始删除时肯定是队首,参数T x是队尾元素,在逻辑的最后要放入最终的空位。

参数Object[] array是二叉堆的数组,参数int n是数组长度。

 

因为是基于数组的队列,其扩容方法可以关注一下:

private void tryGrow(Object[] array, int oldCap) {
    lock.unlock(); // must release and then re-acquire main lock
    Object[] newArray = null;
    if (allocationSpinLock == 0 &&
        UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                 0, 1)) {
        try {
            int newCap = oldCap + ((oldCap < 64) ?
                                   (oldCap + 2) : // grow faster if small
                                   (oldCap >> 1));
            if (newCap - MAX_ARRAY_SIZE > 0) {    // possible overflow
                int minCap = oldCap + 1;
                if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                    throw new OutOfMemoryError();
                newCap = MAX_ARRAY_SIZE;
            }
            if (newCap > oldCap && queue == array)
                newArray = new Object[newCap];
        } finally {
            allocationSpinLock = 0;
        }
    }
    if (newArray == null) // back off if another thread is allocating
        Thread.yield();
    lock.lock();
    if (newArray != null && queue == array) {
        queue = newArray;
        System.arraycopy(array, 0, newArray, 0, oldCap);
    }
}

可以看到,当前容量小于64时,扩容后容量是oldCap*2+2,当前容量大于64时,扩容后容量是oldCap*1.5,容量小的时候扩容快,也就是这段:

int newCap = oldCap + ((oldCap < 64) ?
                       (oldCap + 2) : // grow faster if small
                       (oldCap >> 1));

另外扩容后如果超过了最大容量,新容量就变为最大容量Integer.MAX_VALUE – 8。

 

7,SynchronousQueue

JDK1.5加入。

队列无容量,新增元素时必须等待另一个线程的删除操作才能新增成功。反之亦然。

因为没有容量,peek(),clear()等方法是没有意义的。

不允许null值。

等待中的Producer和Consumer使用同一把锁,节点和item转换靠CAS来实现。

线程池中的Executors.newCachedThreadPool() 使用了这个队列。

出队和入队的策略和LinkedTransferQueue很像,用到了对称操作,put(),take()等方法中调用的都是TransferQueue或TransferStack的transfer(E e, boolean timed, long nanos)方法,根据第一个参数e是否是null来区分是出队还是入队。就像是LinkedTransferQueue 的xfer()方法一样。

SynchronousQueue可使用公平策略或非公平策略,默认非公平策略:

public SynchronousQueue() {
    this(false);
}

public SynchronousQueue(boolean fair) {
    transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}

 

公平策略

使用公平策略时,队列使用TransferQueue,这是SynchronousQueue中的内部类,双向队列,底层是链表,符合先进先出(FIFO)的原则。

TransferQueue中定义了QNode类作为节点类,并维护了head节点和tail节点,不过这两个节点不属于队列中,他们的next节点才是队列的首尾节点。

TransferQueue类的transfer()方法:

E transfer(E e, boolean timed, long nanos) {

    QNode s = null; // constructed/reused as needed
    boolean isData = (e != null);

    for (;;) {
        QNode t = tail;
        QNode h = head;
        if (t == null || h == null)         // saw uninitialized value
            continue;                       // spin

        if (h == t || t.isData == isData) { // empty or same-mode
            QNode tn = t.next;
            if (t != tail)                  // inconsistent read
                continue;
            if (tn != null) {               // lagging tail
                advanceTail(t, tn);
                continue;
            }
            if (timed && nanos <= 0)        // can't wait
                return null;
            if (s == null)
                s = new QNode(e, isData);
            if (!t.casNext(null, s))        // failed to link in
                continue;

            advanceTail(t, s);              // swing tail and wait
            Object x = awaitFulfill(s, e, timed, nanos);
            if (x == s) {                   // wait was cancelled
                clean(t, s);
                return null;
            }

            if (!s.isOffList()) {           // not already unlinked
                advanceHead(t, s);          // unlink if head
                if (x != null)              // and forget fields
                    s.item = s;
                s.waiter = null;
            }
            return (x != null) ? (E)x : e;

        } else {                            // complementary-mode
            QNode m = h.next;               // node to fulfill
            if (t != tail || m == null || h != head)
                continue;                   // inconsistent read

            Object x = m.item;
            if (isData == (x != null) ||    // m already fulfilled
                x == m ||                   // m cancelled
                !m.casItem(x, e)) {         // lost CAS
                advanceHead(h, m);          // dequeue and retry
                continue;
            }

            advanceHead(h, m);              // successfully fulfilled
            LockSupport.unpark(m.waiter);
            return (x != null) ? (E)x : e;
        }
    }
}

出队入队用的都是这个方法,用参数e来判断是出队还是入队,在方法中描述了几个场景:

1,队列为空,或tail节点类型和自己一致,则尝试入队。先要判断tail是否改变,改变了说明有其他线程正在操作,所以要循环重来,然后判断tail是否有next节点,有next说明存在其他线程添加了同类节点,需要更新tail节点然后循环重新重来,前面判断都通过了就新建一个节点作为tail的next节点,然后修改tail节点(这两个操作就可能让此线程会成为前面所谓的其他线程),阻塞此线程并等待被唤醒或超时。

2,tail节点类型和自己不一致,则尝试出队。获得head的next节点(不是head节点,head节点不在队列中),判断节点是否取消,然后交换数据,唤醒对方线程,把对方节点设为head节点(head节点不会被匹配,匹配的是head的next节点)。

 

非公平策略

使用非公平策略时,队列使用TransferStack。这是SynchronousQueue中的内部类,双向栈,底层是链表,符合后进先出(LIFO)的原则。

TransferStack中SNode类作为节点类,并维护了head参数作为栈顶。

TransferStack的transfer()方法:

E transfer(E e, boolean timed, long nanos) {

    SNode s = null; // constructed/reused as needed
    int mode = (e == null) ? REQUEST : DATA;

    for (;;) {
        SNode h = head;
        if (h == null || h.mode == mode) {  // empty or same-mode
            if (timed && nanos <= 0) {      // can't wait
                if (h != null && h.isCancelled())
                    casHead(h, h.next);     // pop cancelled node
                else
                    return null;
            } else if (casHead(h, s = snode(s, e, h, mode))) {
                SNode m = awaitFulfill(s, timed, nanos);
                if (m == s) {               // wait was cancelled
                    clean(s);
                    return null;
                }
                if ((h = head) != null && h.next == s)
                    casHead(h, s.next);     // help s's fulfiller
                return (E) ((mode == REQUEST) ? m.item : s.item);
            }
        } else if (!isFulfilling(h.mode)) { // try to fulfill
            if (h.isCancelled())            // already cancelled
                casHead(h, h.next);         // pop and retry
            else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
                for (;;) { // loop until matched or waiters disappear
                    SNode m = s.next;       // m is s's match
                    if (m == null) {        // all waiters are gone
                        casHead(s, null);   // pop fulfill node
                        s = null;           // use new node next time
                        break;              // restart main loop
                    }
                    SNode mn = m.next;
                    if (m.tryMatch(s)) {
                        casHead(s, mn);     // pop both s and m
                        return (E) ((mode == REQUEST) ? m.item : s.item);
                    } else                  // lost match
                        s.casNext(m, mn);   // help unlink
                }
            }
        } else {                            // help a fulfiller
            SNode m = h.next;               // m is h's match
            if (m == null)                  // waiter is gone
                casHead(h, null);           // pop fulfilling node
            else {
                SNode mn = m.next;
                if (m.tryMatch(h))          // help match
                    casHead(h, mn);         // pop both h and m
                else                        // lost match
                    h.casNext(m, mn);       // help unlink
            }
        }
    }
}

出队入队用的都是这个方法,用参数e来判断是出队还是入队,在方法中描述了几个场景:

1,栈为空,或栈顶元素和本次请求类型相同(比如都是入队请求或者都是出队请求),则判断超时后尝试入栈。

2,栈顶元素和本次请求类型不同,即匹配的场景,而且栈顶元素没有在匹配,则开始与栈顶元素匹配,调用tryMatch()方法尝试匹配,匹配成功后唤醒对方线程,然后两个节点一起出栈。

3,栈顶元素和本次请求类型不同,但栈顶元素正在匹配,则帮助栈顶元素进行匹配,然后循环重试。

所以,虽说SynchronousQueue容量为0,但是队列中依然维护了一个链表,保存了阻塞的线程列表。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章