深读源码-java集合之SynchronousQueue源码分析

问题

(1)SynchronousQueue的实现方式?

(2)SynchronousQueue真的是无缓冲的吗?

(3)SynchronousQueue在高并发情景下会有什么问题?

简介

SynchronousQueue是一个双栈双队列算法,无空间的队列或栈,任何一个对SynchronousQueue写需要等到一个对SynchronousQueue的读操作,反之亦然。一个读操作需要等待一个写操作,相当于是交换通道,提供者和消费者是需要组队完成工作,缺少一个将会阻塞线程,直到等到配对为止。

SynchronousQueue是一个队列和栈算法实现,在SynchronousQueue中双队列FIFO提供公平模式,而双栈LIFO提供的则是非公平模式。

对于SynchronousQueue来说,他的put方法和take方法都被抽象成统一方法来进行操作,通过抽象出内部类Transferer,来实现不同的操作。

但是它有个很大的问题,你知道是什么吗?请看下面的分析。

继承体系

源码分析

主要属性

// CPU的数量
static final int NCPUS = Runtime.getRuntime().availableProcessors();
// 有超时的情况自旋多少次,当CPU数量小于2的时候不自旋
static final int maxTimedSpins = (NCPUS < 2) ? 0 : 32;
// 没有超时的情况自旋多少次
static final int maxUntimedSpins = maxTimedSpins * 16;
// 针对有超时的情况,自旋了多少次后,如果剩余时间大于1000纳秒就使用带时间的LockSupport.parkNanos()这个方法
static final long spinForTimeoutThreshold = 1000L;
// 传输器,即两个线程交换元素使用的东西
private transient volatile Transferer<E> transferer;

通过属性我们可以Get到两个点:

(1)这个阻塞队列里面是会自旋的;

(2)它使用了一个叫做transferer的东西来交换元素;

主要内部类

// Transferer抽象类,主要定义了一个transfer方法用来传输元素
abstract static class Transferer<E> {
    abstract E transfer(E e, boolean timed, long nanos);
}
// 以栈方式实现的Transferer
static final class TransferStack<E> extends Transferer<E> {
    // 栈中节点的几种类型:
    // 1. 消费者(请求数据的)
    static final int REQUEST    = 0;
    // 2. 生产者(提供数据的)
    static final int DATA       = 1;
    // 3. 二者正在匹配中
    static final int FULFILLING = 2;

    // 栈中的节点
    static final class SNode {
        // 下一个节点
        volatile SNode next;        // next node in stack
        // 匹配者
        volatile SNode match;       // the node matched to this
        // 等待着的线程
        volatile Thread waiter;     // to control park/unpark
        // 元素
        Object item;                // data; or null for REQUESTs
        // 模式,也就是节点的类型,是消费者,是生产者,还是正在匹配中
        int mode;
    }
    // 栈的头节点
    volatile SNode head;
}
// 以队列方式实现的Transferer
static final class TransferQueue<E> extends Transferer<E> {
    // 队列中的节点
    static final class QNode {
        // 下一个节点
        volatile QNode next;          // next node in queue
        // 存储的元素
        volatile Object item;         // CAS'ed to or from null
        // 等待着的线程
        volatile Thread waiter;       // to control park/unpark
        // 是否是数据节点
        final boolean isData;
    }

    // 队列的头节点
    transient volatile QNode head;
    // 队列的尾节点
    transient volatile QNode tail;
}

(1)定义了一个抽象类Transferer,里面定义了一个传输元素的方法;

(2)有两种传输元素的方法,一种是栈,一种是队列;

(3)栈的特点是后进先出,队列的特点是先进先出;

(4)栈只需要保存一个头节点就可以了,因为存取元素都是操作头节点;

(5)队列需要保存一个头节点一个尾节点,因为存元素操作尾节点,取元素操作头节点;

(6)每个节点中保存着存储的元素、等待着的线程,以及下一个节点;

(7)栈和队列两种方式有什么不同呢?请看下面的分析。

主要构造方法

public SynchronousQueue() {
    // 默认非公平模式
    this(false);
}

public SynchronousQueue(boolean fair) {
    // 如果是公平模式就使用队列,如果是非公平模式就使用栈
    transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}

(1)默认使用非公平模式,也就是栈结构;

(2)公平模式使用队列,非公平模式使用栈;

公平模式

TransferQueue内部是如何进行工作的,这里先大致讲解下,队列采用了互补模式进行等待,QNode中有一个字段是isData,如果模式相同或空队列时进行等待操作,互补的情况下就进行消费操作。

入队操作相同模式

 不同模式时进行出队列操作:

这时候来了一个isData=false的互补模式,队列就会变成如下状态:

transfer()方法

transfer()方法同时实现了取元素和放元素的功能,下面我再来看看这个transfer()方法里究竟干了什么。

E transfer(E e, boolean timed, long nanos) {
    QNode s = null; // constructed/reused as needed
    // 分为两种状态1.有数据=true 2.无数据=false
    boolean isData = (e != null);
    // 循环内容
    for (;;) {
        // 尾部节点。
        QNode t = tail;
        // 头部节点。
        QNode h = head;
        // 判断头部和尾部如果有一个为null则自旋转。
        if (t == null || h == null)         // 还未进行初始化的值。
            continue;                       // 自旋
        // 头结点和尾节点相同或者尾节点的模式和当前节点模式相同。
        if (h == t || t.isData == isData) { // 空或同模式。
            // tn为尾节点的下一个节点信息。
            QNode tn = t.next;
            // 这里我认为是阅读不一致,原因是当前线程还没有阻塞的时候其他线程已经修改了尾节点tail会导致当前线程的tail节点不一致。
            if (t != tail)                  // inconsistent read
                continue;
            if (tn != null) {               // lagging tail
                advanceTail(t, tn);
                continue;
            }
            if (timed && nanos <= 0)        // 这里如果指定timed判断时间小于等于0直接返回。
                return null;
            // 判断新增节点是否为null,为null直接构建新节点。
            if (s == null)
                s = new QNode(e, isData);
            if (!t.casNext(null, s))        // 如果next节点不为null说明已经有其他线程进行tail操作
                continue;
            // 将t节点替换为s节点
            advanceTail(t, s);
            // 等待有消费者消费线程。
            Object x = awaitFulfill(s, e, timed, nanos);
            // 如果返回的x,指的是s.item,如果s.item指向自己的话清除操作。
            if (x == s) {
                clean(t, s);
                return null;
            }
            // 如果没有取消联系
            if (!s.isOffList()) {
                // 将当前节点替换头结点
                advanceHead(t, s);          // unlink if head
                if (x != null)              // 取消item值,这里是take方法时会进行item赋值为this
                    s.item = s;
                // 将等待线程设置为null
                s.waiter = null;
            }
            return (x != null) ? (E)x : e;

        } else {                            // complementary-mode
            // 获取头结点下一个节点
            QNode m = h.next;               // node to fulfill
            // 如果当前线程尾节点和全局尾节点不一致,重新开始
            // 头结点的next节点为空,代表无下一个节点,则重新开始,
            // 当前线程头结点和全局头结点不相等,则重新开始
            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;
        }
    }
}

我们来看一下awaitFulfill方法内容:

Object awaitFulfill(QNode s, E e, boolean timed, long nanos) {
    // 如果指定了timed则为System.nanoTime() + nanos,反之为0。
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    // 获取当前线程。
    Thread w = Thread.currentThread();
    // 如果头节点下一个节点是当前s节点(以防止其他线程已经修改了head节点)
    // 则运算(timed ? maxTimedSpins : maxUntimedSpins),否则直接返回。
    // 指定了timed则使用maxTimedSpins,反之使用maxUntimedSpins
    int spins = ((head.next == s) ?
                 (timed ? maxTimedSpins : maxUntimedSpins) : 0);
    // 自旋
    for (;;) {
        // 判断是否已经被中断。
        if (w.isInterrupted())
            //尝试取消,将当前节点的item修改为当前节点(this)。
            s.tryCancel(e);
        // 获取当前节点内容。
        Object x = s.item;
        // 判断当前值和节点值不相同是返回,因为弹出时会将item值赋值为null。
        if (x != e)
            return x;
        if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                s.tryCancel(e);
                continue;
            }
        }
        if (spins > 0)
            --spins;
        else if (s.waiter == null)
            s.waiter = w;
        else if (!timed)
            LockSupport.park(this);
        else if (nanos > spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanos);
    }
}

1.首先先判断有没有被中断,如果被中断则取消本次操作,将当前节点的item内容赋值为当前节点。

2.判断当前节点和节点值不相同是返回

3.将当前线程赋值给当前节点

4.自旋,如果指定了timed则使用LockSupport.parkNanos(this, nanos);,如果没有指定则使用LockSupport.park(this);

5.中断相应是在下次才能被执行。

 通过上面源码分析我们这里做出简单的示例代码演示一下put操作和take操作是如何进行运作的,首先看一下示例代码,如下所示:

public class SynchronousQueueDemo {
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        SynchronousQueue<Integer> queue = new SynchronousQueue<>(true);
        Thread thread1 = new Thread(() -> {
            try {
                queue.put(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread1.start();
        Thread.sleep(2000);
        Thread thread2 = new Thread(() -> {
            try {
                queue.put(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread2.start();
        Thread.sleep(10000);
        Thread thread3 = new Thread(() -> {
            try {
                System.out.println(queue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread3.start();
    }
}

 首先上来之后进行的是两次take操作,然后再put操作,默认队列上来会进行初始化,初始化的内容如下代码所示:

TransferQueue() {
    QNode h = new QNode(null, false); // initialize to dummy node.
    head = h;
    tail = h;
}

初始化后队列的状态如下图所示:

 当线程1执行put操作时,来分析下代码:

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

首先执行局部变量t代表队尾指针,h代表队头指针,判断队头和队尾不为空则进行下面的操作,接下来是if…else语句这里是分水岭,当相同模式操作的时候执行if语句,当进行不同模式操作时执行的是else语句,程序是如何控制这样的操作的呢?接下来我们慢慢分析一下:

if (h == t || t.isData == isData) { // 队列为空或者模式相同时进行if语句
    QNode tn = t.next;
    if (t != tail)                  // 判断t是否是队尾,不是则重新循环。
        continue;
    if (tn != null) {               // tn是队尾的下个节点,如果tn有内容则将队尾更换为tn,并且重新循环操作。
        advanceTail(t, tn);
        continue;
    }
    if (timed && nanos <= 0)        // 如果指定了timed并且延时时间用尽则直接返回空,这里操作主要是offer操作时,因为队列无存储空间的当offer时不允许插入。
        return null;
    if (s == null)                  // 这里是新节点生成。
        s = new QNode(e, isData);
    if (!t.casNext(null, s))        // 将尾节点的next节点修改为当前节点。
        continue;

    advanceTail(t, s);              // 队尾移动
    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;

}

上面代码是if语句中的内容,进入到if语句中的判断是如果头结点和尾节点相等代表队列为空,并没有元素所有要进行插入队列的操作,或者是队尾的节点的isData标志和当前操作的节点的类型一样时,会进行入队操作,isData标识当前元素是否是数据,如果为true代表是数据,如果为false则代表不是数据,换句话说只有模式相同的时候才会往队列中存放,如果不是模式相同的时候则代表互补模式,就不走if语句了,而是走了else语句,上面代码中做有注释讲解,下面看一下这里:

if (s == null)                  // 这里是新节点生成。
    s = new QNode(e, isData);
if (!t.casNext(null, s))        // 将尾节点的next节点修改为当前节点。
    continue

当执行上面代码后,队列的情况如下图所示:(这里视为插入第一个元素图,方便下面的引用)

接下来执行这段代码:

advanceTail(t, s); // 队尾移动 

 修改了tail节点后,这时候就需要进行自旋操作,并且设置QNode的waiter等待线程,并且将线程等待,等到唤醒线程进行唤醒操作

Object x = awaitFulfill(s, e, timed, nanos);	//自旋并且设置线程

方法内部分析局部内容,上面已经全部内容的分析:

if (spins > 0)
--spins;
else if (s.waiter == null)
s.waiter = w;
else if (!timed)
LockSupport.park(this);
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos); 

如果自旋时间spins还有则进行循环递减操作,接下来判断如果当前节点的waiter是空则价格当前线程赋值给waiter,上图中显然是为空的所以会把当前线程进行赋值给我waiter,接下来就是等待操作了。

 上面线程则处于等待状态,接下来是线程二进行操作,这里不进行重复进行,插入第二个元素队列的状况,此时线程二也处于等待状态。

上面的主要是put了两次操作后队列的情况,接下来分析一下take操作时又是如何进行操作的,当take操作时,isData为false,而队尾的isData为true两个不相等,所以不会进入到if语句,而是进入到了else语句

} else {                            // 互补模式
    QNode m = h.next;               // 获取头结点的下一个节点,进行互补操作。
    if (t != tail || m == null || h != head)
        continue;                   // 这里就是为了防止阅读不一致的问题

    Object x = m.item;
    if (isData == (x != null) ||    // 如果x=null说明已经被读取了。
        x == m ||                   // x节点和m节点相等说明被中断操作,被取消操作了。
        !m.casItem(x, e)) {         // 这里是将item值设置为null
        advanceHead(h, m);          // 移动头结点到头结点的下一个节点
        continue;
    }

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

首先获取头结点的下一个节点用于互补操作,也就是take操作,接下来进行阅读不一致的判断,防止其他线程进行了阅读操作,接下来获取需要弹出内容x=1,首先进行判断节点内容是不是已经被消费了,节点内容为null时则代表被消费了,接下来判断节点的item值是不是和本身相等如果相等话说明节点被取消了或者被中断了,然后移动头结点到下一个节点上,然后将refenrence-715的item值修改为null,至于为什么修改为null这里留下一个悬念,这里还是比较重要的,大家看到这里的时候需要注意下,显然这些都不会成立,所以if语句中内容不会被执行,接下来的队列的状态是是这个样子的: 

OK,接下来就开始移动队头head了,将head移动到m节点上,执行代码如下所示:

advanceHead(h, m); 

此时队列的状态是这个样子的:

 LockSupport.unpark(m.waiter);
return (x != null) ? (E)x : e;

接下来将执行唤醒被等待的线程,也就是thread-0,然后返回获取item值1,take方法结束,但是这里并没有结束,因为唤醒了put的线程,此时会切换到put方法中,这时候线程唤醒后会执行awaitFulfill方法,此时循环时,有与item值修改为null则直接返回内容。

Object x = s.item;
if (x != e)
    return x;

这里的代码我们可以对照插入第一个元素图,s节点也就是当前m节点,获取值得时候已经修改为null,但是当时插入的值时1,所以两个不想等了,则直接返回null值。

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;

又返回到了transfer方法的if语句中,此时x和s并不相等所以不用进行clean操作,首先判断s节点是否已经离队了,显然并没有进行离队操作,advanceHead(t, s);操作不会被执行因为上面已近将头节点修改了,但是第一次插入的时候头结点还是reference-716,此时已经是reference-715,而t节点的引用地址是reference-716,所以不会操作,接下来就是将waiter设置为null,也就是忘记掉等待的线程。

 分析了正常的take和put操作,接下来分析下中断操作,由于中断相应后,会被执行if(w.isInterrupted())这段代码,它会执行s.tryCancel(e)方法,这个方法的作用的是将QNode节点的item节点赋值为当前QNode,这时候x和e值就不相等了(if (x != e)),x的值是s.item,则为当前QNode,而e的值是用户指定的值,这时候返回x(s.item)。返回到函数调用地方transfer中,这时候要执行下面语句:

if (x == s) {
    clean(t, s);
    return null;
}

进入到clean方法执行清理当前节点,下面是方法clean代码:

/**
 * Gets rid of cancelled node s with original predecessor pred.
 */
void clean(QNode pred, QNode s) {
    s.waiter = null; // forget thread
    /*
     * At any given time, exactly one node on list cannot be
     * deleted -- the last inserted node. To accommodate this,
     * if we cannot delete s, we save its predecessor as
     * "cleanMe", deleting the previously saved version
     * first. At least one of node s or the node previously
     * saved can always be deleted, so this always terminates.
     */
    while (pred.next == s) { // Return early if already unlinked
        QNode h = head;
        QNode hn = h.next;   // Absorb cancelled first node as head
        if (hn != null && hn.isCancelled()) {
            advanceHead(h, hn);
            continue;
        }
        QNode t = tail;      // Ensure consistent read for tail
        if (t == h)
            return;
        QNode tn = t.next;
        // 判断现在的t是不是末尾节点,可能其他线程插入了内容导致不是最后的节点。
        if (t != tail)
            continue;
        // 如果不是最后节点的话将其现在t.next节点作为tail尾节点。
        if (tn != null) {
            advanceTail(t, tn);
            continue;
        }
        // 如果当前节点不是尾节点进入到这里面。
        if (s != t) {        // If not tail, try to unsplice
            // 获取当前节点(被取消的节点)的下一个节点。
            QNode sn = s.next;
            // 修改上一个节点的next(下一个)元素为下下个节点。
            if (sn == s || pred.casNext(s, sn))
                //返回。
                return;
        }
        QNode dp = cleanMe;
        if (dp != null) {    // 尝试清除上一个标记为清除的节点。
            QNode d = dp.next;	//1.获取要被清除的节点
            QNode dn;
            if (d == null ||               // 被清除节点不为空
                d == dp ||                 // 被清除节点已经离队
                !d.isCancelled() ||        // 被清除节点是标记为Cancel状态的。
                (d != t &&                 // 被清除节点不是尾节点
                 (dn = d.next) != null &&  // 被清除节点下一个节点不为null
                 dn != d &&                //   that is on list
                 dp.casNext(d, dn)))       // 将被清除的节点的前一个节点的下一个节点修改为被清除节点的下一个节点。
                casCleanMe(dp, null);      // 清空cleanMe节点。
            if (dp == pred)
                return;      // s is already saved node
        } else if (casCleanMe(null, pred)) // 这里将上一个节点标记为被清除操作,但是其实要操作的是下一个节点。
            return;          // Postpone cleaning s
    }
}

1.如果节点中取消的头结点的下一个节点,只需要移动当前head节点到下一个节点即可。

2.如果取消的是中间的节点,则将当前节点next节点修改为下下个节点。

3.如果修改为末尾的节点,则将当前节点放入到QNode的clearMe中,等待有内容进来之后下一次进行清除操作。

实例一:清除头结点下一个节点,下面是实例代码进行讲解:

/**
 * 清除头结点的下一个节点实例代码。
 *
 */
public class SynchronousQueueDemo {
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        SynchronousQueue<Integer> queue = new SynchronousQueue<>(true);
        AtomicInteger atomicInteger = new AtomicInteger(0);

        Thread thread1 = new Thread(() -> {
            try {
                queue.put(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread1.start();
        Thread.sleep(200);

        Thread thread2 = new Thread(() -> {
            try {
                queue.put(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread2.start();
      	Thread.sleep(2000);
        thread1.interrupt();

    }
}

上面例子说明我们启动了两个线程,分别向SynchronousQueue队列中添加了元素1和元素2,添加成功之后的,让主线程休眠一会,然后将第一个线程进行中断操作,添加两个元素后节点所处在的状态为下图所示:

 当我们调用thread1.interrupt时,此时线程1等待的消费操作将被终止,会相应上面awaitFulfill方法,该方法会运行下面代码:

if (w.isInterrupted())
    //尝试取消,将当前节点的item修改为当前节点(this)。
    s.tryCancel(e);
// 获取当前节点内容。
Object x = s.item;
// 判断当前值和节点值不相同是返回,因为弹出时会将item值赋值为null。
if (x != e)
    return x;

首先上来现将s节点(上图中的Reference-715引用对象)的item节点设置为当前节点引用(Reference-715引用对象),所以s节点和e=1不相等则直接返回,此时节点的状态变化如下所示:

 退出awaitFulfill并且返回的是s节点内容(实际上返回的就是s节点),接下来返回到调用awaitFulfill的方法transfer方法中

Object x = awaitFulfill(s, e, timed, nanos);
if (x == s) {                   // 是否是被取消了
    clean(t, s);
    return null;
}

首先判断的事x节点和s节点是否相等,上面我们也说了明显是相等的所以这里会进入到clean方法中,clean(QNode pred, QNode s)clean方法一个是前节点,一个是当前被取消的节点,也就是当前s节点的前节点是head节点,接下来我们一步一步的分析代码:

s.waiter = null; // 删除等待的线程。

进入到方法体之后首先先进行的是将当前节点的等待线程删除,如下图所示:

 接下来进入while循环,循环内容时pred.next == s如果不是则表示已经移除了节点,反之还在队列中,则进行下面的操作:

QNode h = head;
QNode hn = h.next;   // 如果取消的是第一个节点则进入下面语句
if (hn != null && hn.isCancelled()) {
    advanceHead(h, hn);
    continue;
}

可以看到首先h节点为head节点,hn为头结点的下一个节点,在进行判断头结点的下一个节点不为空并且头结点下一个节点是被中断的节点(取消的节点),则进入到if语句中,if语句其实也很简单就是将头结点修改为头结点的下一个节点(s节点,别取消节点,并且将前节点的next节点修改为自己,也就是移除了之前的节点,我们看下advanceHead方法:

void advanceHead(QNode h, QNode nh) {
    if (h == head &&
        UNSAFE.compareAndSwapObject(this, headOffset, h, nh))
        h.next = h; // forget old next
}

首先上来先进行CAS移动头结点,再讲原来头结点h的next节点修改为自己(h),为什么这样做呢?因为上面进行advanceHead之后并没有退出循环,是进行continue操作,也就是它并没有跳出while循环,他还会循环一次prev.next此时已经不能等于s所以退出循环,如下图所示:

实例二:清除中间的节点

public class SynchronousQueueDemo {
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        SynchronousQueue<Integer> queue = new SynchronousQueue<>(true);
        AtomicInteger atomicInteger = new AtomicInteger(0);

        Thread thread1 = new Thread(() -> {
            try {
                queue.put(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread1.start();
        //休眠一会。
        Thread.sleep(200);
        Thread thread2 = new Thread(() -> {
            try {
                queue.put(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread2.start();
        //休眠一会。
        Thread.sleep(200);
        Thread thread3 = new Thread(() -> {
            try {
                queue.put(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread3.start();
        //休眠一会。
        Thread.sleep(10000);
        thread2.interrupt();


    }
}

看上面例子,首先先进行put操作三次,也就是入队3条数据,分别是整型值1,整型值2,整型值3,然后将当前线程休眠一下,对中间线程进行中断操作,通过让主线程休眠一会保证线程执行顺序性(当然上面线程不一定能保证执行顺序,因为put操作一下子就执行完了所以这点时间是可以的),此时队列所处的状态来看一下下图:

 当休眠一会之后,进入到threa2进行中断操作,目前上图中表示Reference-723被中断操作,此时也会进入到awaitFulfill方法中,将Reference-723的item节点修改为当前节点,如下图所示:

进入到clear方法中此时的prev节点为Reference-715,s节点是被清除节点,还是首先进入clear方法中先将waiter设置为null,取消当前线程内容,如下图所示:

 接下来进入到循环中,进行下面处理

QNode h = head;
QNode hn = h.next;   // Absorb cancelled first node as head
if (hn != null && hn.isCancelled()) {
    advanceHead(h, hn);
    continue;
}
QNode t = tail;      // Ensure consistent read for tail
if (t == h)
    return;
QNode tn = t.next;
if (t != tail)
    continue;
if (tn != null) {
    advanceTail(t, tn);
    continue;
}
if (s != t) {        // If not tail, try to unsplice
    QNode sn = s.next;
    if (sn == s || pred.casNext(s, sn))
        return;
}

第一个if语句已经分析过了所以说这里不会进入到里面去,接下来是进行尾节点t是否是等于head节点如果相等则代表没有元素,在判断当前方法的t尾节点是不是真正的尾节点tail如果不是则进行修改尾节点,先来看一下现在的状态:

 tn != null判断如果tn不是尾节点,则将tn作为尾节点处理,如果处理之后还不是尾节点还会进行处理直到tail是尾节点未知,我们现在这个是尾节点所以跳过这段代码。s != t通过上图可以看到s节点是被清除节点,并不是尾节点所以进入到循环中:

if (s != t) {        // If not tail, try to unsplice
    QNode sn = s.next;
    if (sn == s || pred.casNext(s, sn))
        return;
}

首先获取的s节点的下一个节点,上图中表示Reference-725节点,判断sn是都等于当前节点显然这一条不成立,pred节点为Reference-715节点,将715节点的next节点变成Reference-725节点,这里就将原来的节点清理出去了,现在的状态如下所示:

实例三:删除的节点是尾节点

/**
 * SynchronousQueue实例三,删除的节点为尾节点
 *
 */
public class SynchronousQueueDemo {
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        SynchronousQueue<Integer> queue = new SynchronousQueue<>(true);
        AtomicInteger atomicInteger = new AtomicInteger(0);

        Thread thread1 = new Thread(() -> {
            try {
                queue.put(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread1.start();

        Thread thread2 = new Thread(() -> {
            try {
                queue.put(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread2.start();

        Thread.sleep(10000);
        thread2.interrupt();

        Thread.sleep(10000);

        Thread thread3 = new Thread(() -> {
            try {
                queue.put(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread3.start();

        Thread.sleep(10000);
        thread3.interrupt();
    }
}

该例子主要说明一个问题就是删除的节点如果是末尾节点的话,clear方法又是如何处理的,首先启动了三个线程其中主线程休眠了一会,为了能让插入的顺序保持线程1,线程2,线程3这样子,启动第二个线程后,又将第二个线程中断,这是第二个线程插入的节点为尾节点,然后再启动第三个节点插入值,再中断了第三个节点末尾节点,说一下为啥这样操作,因为当清除尾节点时,并不是直接移除当前节点,而是将被清除的节点的前节点设置到QNode的CleanMe中,等待下次clear方法时进行清除上次保存在CleanMe的节点,然后再处理当前被中断节点,将新的被清理的节点prev设置为cleanMe当中,等待下次进行处理,接下来一步一步分析,首先我们先来看一下第二个线程启动后节点的状态。

 此时运行thread2.interrupt();将第二个线程中断,这时候会进入到clear方法中,前面的代码都不会被返回,会执行下面的语句:

QNode dp = cleanMe;
if (dp != null) {    // Try unlinking previous cancelled node
    QNode d = dp.next;
    QNode dn;
    if (d == null ||               // d is gone or
        d == dp ||                 // d is off list or
        !d.isCancelled() ||        // d not cancelled or
        (d != t &&                 // d not tail and
         (dn = d.next) != null &&  //   has successor
         dn != d &&                //   that is on list
         dp.casNext(d, dn)))       // d unspliced
        casCleanMe(dp, null);
    if (dp == pred)
        return;      // s is already saved node
} else if (casCleanMe(null, pred))
    return;

首先获得TransferQueue当中cleanMe节点,此时获取的为null,当判断dp!=null时就会被跳过,直接执行

casCleanMe(null, pred)此时pred传入的值时t节点指向的内容,也就是当前节点的上一个节点,它会被标记为清除操作节点(其实并不清楚它而是清除它下一个节点,也就是说item=this的节点),此时看一下节点状态为下图所示:

 接下来第三个线程启动了这时候又往队列中添加了元素3,此时队列的状况如下图所示:

此时thread3也被中断操作了,这时候还是运行上面的代码,但是这次不同的点在于cleanMe已经不是空值,是有内容的,首先获取的是cleanMe的下一个节点(d),然我来把变量标记在图上然后看起来好分析一些,如下图所示:

 dp表示d节点的前一个pred节点,dn表示d节点的next节点,主要逻辑在这里:

if (d == null ||               // d is gone or
    d == dp ||                 // d is off list or
    !d.isCancelled() ||        // d not cancelled or
    (d != t &&                 // d not tail and
     (dn = d.next) != null &&  //   has successor
     dn != d &&                //   that is on list
     dp.casNext(d, dn)))       // d unspliced
    casCleanMe(dp, null);
if (dp == pred)
    return;      // s

首先判断d节点是不是为null,如果d节点为null代表已经清除掉了,如果cleanMe节点的下一个节点和自己相等,说明需要清除的节点已经离队了,判断下个节点是不是需要被清除的节点,目前看d节点是被清除的节点,然后就将被清除的节点的下一个节点赋值给dn并且判断d节点是不是末尾节点,如果不是末尾节点则进行dp.casNext方法,这个地方是关键点,它将被清除节点d的前节点的next节点修改为被清除节点d的后面节点dn,然后调用caseCleanMe将TransferQueue中的cleanMe节点清空,此时节点的内容如下所示:

 可以看出将上一次标记为清除的节点清除了队列中,清除完了就完事儿?那这次的怎么弄呢?因为现在运行的是thread3的中断程序,所以上面并没有退出,而是再次进入循环,循环之后发现dp为null则会运行casCleanMe(null, pred),此时当前节点s的前一个节点已经被清除队列,但是并不影响后续的清除操作,因为前节点的next节点还在维护中,也是前节点的next指向还是reference-725,如下图所示:

就此分析完毕如果有不正确的地方请指正。 

非公平模式

非公平模式下的SynchronousQueue是如何进行工作的,在源码分析的时候,先来简单看一下非公平模式的简单原理,它采用的栈这种FILO先进后出的方式进行非公平处理,它内部有三种状态,分别是REQUEST,DATA,FULFILLING,其中REQUEST代表的数据请求的操作也就是take操作,而DATA表示的是数据也就是Put操作将数据存放到栈中,用于消费者进行获取操作,而FULFILLING代表的是可以进行互补操作的状态,其实和前面讲的公平模式也很类似。

当有相同模式情况下进行入栈操作,相同操作指的是REQUEST和DATA两种类型中任意一种进行操作时,模式相同则进行入栈操作,如下图所示:

同REQUEST进行获取数据时的入栈情况:

同样的put的操作,进行数据操作时为DATA类型的操作,此时队列情况为:

 不同模式下又是如何进行操作的?当有不同模式进来的时候,他不是将当前的模式压入栈顶,而是将FullFill模式和当前模式进行按位或之后压入栈顶,也就是压入一个进行FullFill请求的模式进入栈顶,请求配对操作,如下图所示:

通过上图可见,本来栈中有一个DATA模式的数据等待消费者进行消费,这时候来了一个REQUEST模式的请求操作来进行消费数据,这时候并没有将REQUEST模式直接压入栈顶,而是将其转换为FULLFILLING模式,并且保留了原有的类型,这是进行FULLFILLING的请求,请求和栈顶下方元素进行匹配,当匹配成功后将栈顶和匹配元素同时进行出栈操作,详细请见下文分析:

 transfer()方法

transfer()方法同时实现了取元素和放元素的功能,下面我再来看看这个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) {  // 栈顶指针为空或者是模式相同
            if (timed && nanos <= 0) {      // 制定了timed并且时间小于等于0则取消操作。
                if (h != null && h.isCancelled())
                    casHead(h, h.next);     // 判断头结点是否被取消了取消了就弹出队列,将头结点指向下一个节点
                else
                    return null;
            } else if (casHead(h, s = snode(s, e, h, mode))) {// 初始化新节点并且修改栈顶指针
                SNode m = awaitFulfill(s, timed, nanos);			// 进行等待操作
                if (m == s) {               // 返回内容是本身则进行清理操作
                    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)) { // 尝试去匹配
            if (h.isCancelled())            // 判断是否已经被取消了
                casHead(h, h.next);         // 弹出取消的节点并且从新进入主循环
            else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {//新建一个Full节点压入栈顶
                for (;;) { // 循环直到匹配
                    SNode m = s.next;       // s的下一个节点为匹配节点
                    if (m == null) {        // 代表没有等待内容了
                        casHead(s, null);   // 弹出full节点
                        s = null;           // 设置为null用于下次生成新的节点
                        break;              // 退回到主循环中
                    }
                    SNode mn = m.next;
                    if (m.tryMatch(s)) {
                        casHead(s, mn);     // 弹出s节点和m节点两个节点
                        return (E) ((mode == REQUEST) ? m.item : s.item);
                    } else                  // 如果失去了匹配
                        s.casNext(m, mn);   // 帮助取消连接
                }
            }
        } else {                            // 这里是帮助进行fillull
            SNode m = h.next;               // m是头结点的匹配节点
            if (m == null)                  // 如果m不存在则直接将头节点赋值为nll
                casHead(h, null);           // 弹出fulfill节点
            else {
                SNode mn = m.next;
                if (m.tryMatch(h))          // h节点尝试匹配m节点
                    casHead(h, mn);         // 弹出h和m节点
                else                        // 丢失匹配则直接将头结点的下一个节点赋值为头结点的下下节点
                    h.casNext(m, mn);       
            }
        }
    }
}

1.模式相同的时候则进行等待操作,入队等待操作

2.当模式不相同时,首先判断头结点是否是fulfill节点如果不是则进行匹配操作,如果是fulfill节点先帮助头结点的fulfill节点进行匹配操作

接下来再来看一下awaitFulfill方法内容

SNode awaitFulfill(SNode s, boolean timed, long nanos) {
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
  	// 等待线程
    Thread w = Thread.currentThread();
  	// 等待时间设置
    int spins = (shouldSpin(s) ?
                 (timed ? maxTimedSpins : maxUntimedSpins) : 0);
    for (;;) {
        if (w.isInterrupted())			// 判断当前线程是否被中断 
            s.tryCancel();					// 尝试取消操作 
        SNode m = s.match;					// 获取当前节点的匹配节点,如果节点不为null代表匹配或取消操作,则返回
        if (m != null)
            return m;
        if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                s.tryCancel();
                continue;
            }
        }
        if (spins > 0)
            spins = shouldSpin(s) ? (spins-1) : 0;
        else if (s.waiter == null)
            s.waiter = w; // establish waiter so can park next iter
        else if (!timed)
            LockSupport.park(this);
        else if (nanos > spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanos);
    }
}

通过上面的源码,其实我们之前分析同步模式的时候差不太多,变化的地方其中包括返回内容判断这里判断的是match节点是否为null,还有就是spins时间设置这里发现了shoudSpin用来判断是否进行轮训,来看一下shouldSpin方法:

/**
 * 判断节点是否是fulfill节点,或者是头结点为空再或者是头结点和当前节点相等时则不需要进行轮训操作
 */
boolean shouldSpin(SNode s) {
    SNode h = head;
    return (h == s || h == null || isFulfilling(h.mode));
}

实际上就是判断节点是否是fulfill节点,或者是头结点为空再或者是头结点和当前节点相等时则不需要进行轮训操作,如果满足上述条件就不小进行轮训等到操作了直接进行等待就行了。

接下来我们来用例子一点点解析原理:

首先先进行一个put操作,这样可以简单分析下内部信息。

/**
 * SynchronousQueue原理内容
 *
 * @author battleheart
 */
public class SynchronousQueueDemo1 {
    public static void main(String[] args) throws Exception {
        SynchronousQueue<Integer> queue = new SynchronousQueue<>();

        Thread thread1 = new Thread(() -> {
            try {
                queue.put(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread1.start();
    }
}

首先它会进入到transfer方法中,进行第一步的判断他的类型信息,如下所示:

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

通过上面代码可以看到e=1所以是DATA类型,接下来进行判断是如何进行操作,当前堆栈是空的,如何判断堆栈为空呢?上面也讲到了head节点为空时则代表堆栈为空,接下来就要判断如果head节点为空或head指向的节点和当前操作内容模式相同,则进行等待操作,如下代码所示:

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

显然头结点是空的,所以进入到第一个fi语句中执行等待操作,如果指定了timed则判断时间是否小于0,如果小于0则直接null,反之判断当前节点是否不是头结点以及头结点是否取消,潘祖条件弹出头结点,并将下一个节点设置为头结点,上述条件在当前例子中都不满足,所以要进入到下面这段代码中,首先进行对s进行初始化值,并且进行入栈操作,casHead(h, s = snode(s, e, h, mode)),下面看一下栈中的情况如下图所示:

 当执行完了入栈操作之后接下来要执行awaitFulfill这里的操作就是轮训以及将当前节点的线程赋值,并且挂起当前线程。此时的栈的情况如下图所示:

当有同样的模式进行操作时候也是重复上述的操作内容,我们这里模拟两次put操作,让让我们看一下栈中的情况如下图所示:

 通过上图可以看到,其实就是将头结点移动到了新的节点上,然后新节点的next节点维护这下一个节点的引用,好了,上述内容分析是同模式的操作,接下来我们试着进行take操作时,这时候会发什么内容呢?

/**
 * SynchronousQueue例子二进行两次put操作和一次take操作
 *
 * @author battleheart
 */
public class SynchronousQueueDemo1 {
    public static void main(String[] args) throws Exception {
        SynchronousQueue<Integer> queue = new SynchronousQueue<>();

        Thread thread1 = new Thread(() -> {
            try {
                queue.put(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread1.start();
        Thread.sleep(2000);
        Thread thread2 = new Thread(() -> {
            try {
                queue.put(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread2.start();

        Thread.sleep(2000);
        Thread thread6 = new Thread(() -> {
            try {
                queue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
      	thread6.start();
    }
}

上面例子正好符合上面例子两次put操作的截图,进行两次put操作过后再进行take操作,接下来我们来看一下take操作是如何进行操作的,换句话说当有不同模式的操作时又是如何进行处理呢?上面分析的内容是同种操作模式下的,当有不同操作则会走下面内容:

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

最下面的else我们等会来进行分析,我们看到如果不是同模式的话,则会先判断是否是fulfill模式,如果不是fulfill模式,则进入到第一个if语句中,显然通过图示6可以得出,头结点head模式并不是fillfull模式,则进入到该if语句中,上来首先判断当前头结点是否被取消了,如果被取消则将头结点移动到栈顶下一个节点,反之则将s节点赋值为fulfill模式按位或当前节点模式,个人认为目的是既保留了原有模式也变成了fulfill模式,我们开篇就讲到了,REQUEST=0,二进制则是00,而DATA=1,其二进制为01,而FULFILLING=2,其二进制表示10,也就是说如果当前节点是REQUEST的话那么节点的内容值时00|10=10,如果节点是DATA模式则s节点的模式时01|10=11,这样的话11既保留了原有模式也是FULFILLING模式,然后将头节点移动到当前s节点,也就是将FULFILLING模式节点入栈操作,目前分析到这里时casHead(h, s=snode(s, e, h, FULFILLING|mode),栈的情况如下图所示:

 接下来运行for循环里面内容,先运行如下内容:

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
}

先判断当前节点也就是头结点s的下一个节点上图中head=s节点,所以s.next节点代表的是Ref-750,判断当前节点是否为空,如果为空的话代表没有可匹配的节点,先对head进行替换为null代表堆栈为空,然后将当前s节点设置为null,退出fulfill匹配模式进入到主循环中,会重新进行对当前节点进行操作,是消费还是匹配,显然本例子中m节点是不为空的,所以这里不会运行,跳过之后运行下面内容:

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 

mn节点在上图中对应的是Ref-681,这里是重点,m.tryMatch(s),m节点尝试匹配s节点,进入到方法里,到这一步是我们再来看一下头结点的元素的内容:

 并且唤醒m节点的,告诉m节点,你现在有匹配的对象了你可以被唤醒了,这里唤醒之后就会进入到awaitFulfill下面的操作

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

运行这里的线程显然是上图中的m节点,因为m节点被唤醒了,m==s代表的是取消了节点,显然没有进行该操作,然后就是帮助头结点进行fulfill操作,这里重点说一下这段代码:

if ((h = head) != null && h.next == s)
    casHead(h, s.next);

获取当前头结点,也就是上图中的头结点如果不为空而且h.next节点为m节点正好是m节点进行操作时的s节点,也就是说这个语句是成立的,直接将头节点指向了上图的mn节点,这里的操作和take中的下面操作是一样的,也就是帮助fulfill操作弹出栈顶和栈顶匹配的节点内容,下面代码:

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

重点是casHead的代码,弹出s和m两个节点,此时栈中内容如下图所示:

 主要的流程分析完毕了,但是细心的朋友会发现,最后面还有一个帮助fulfill的操作,(transfer中)代码如下所示:

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

个人理解是这样的,我们上面也分析到了如果模式是相同模式情况和如果是不同模式且模式不为匹配模式的情况,但是还会有另外一种情况就是如果是不同模式并且头结点是匹配模式的就会进入到帮助去fullfill的情况,我来画图说明一下该情况:

 如上图所示,上一个匹配操作没有进行完然后又来了一个请求操作,他就会帮助head进行匹配操作,也就是运行上面的代码逻辑,逻辑和匹配内容是一样的。

接下来让我们看一下取消的clean方法内容:

void clean(SNode s) {
    s.item = null;   // 将item值设置为null
    s.waiter = null; // 将线程设置为null

    SNode past = s.next;   // s节点下一个节点如果不为空,并且节点是取消节点则指向下下个节点,这里是结束的标识,代表没有了。
    if (past != null && past.isCancelled())
        past = past.next;

    // 如果取消的是头节点则运行下面的清理操作,操作逻辑很简单就是判断头结点是不是取消节点,如果是则将节点一定到下一个节点
    SNode p;
    while ((p = head) != null && p != past && p.isCancelled())
        casHead(p, p.next);

    // 取消不是头结点的嵌套节点。
    while (p != null && p != past) {
        SNode n = p.next;
        if (n != null && n.isCancelled())
            p.casNext(n, n.next);
        else
            p = n;
    }
}

通过源码可以看到首先是先找到一个可以结束的标识past,也就说到这里就结束了,判断是否不是头节点被取消了,如果是头节点被取消了则进行第一个while语句,操作也很简单就是将头节点替换头结点的下一个节点,如果不是头节点被取消了则进行下面的while语句操作,其实就是将取消的上一个节点的下一个节点指定为被取消节点的下一个节点,到此分析完毕了。 

总结

(1)SynchronousQueue是java里的无缓冲队列,用于在两个线程之间直接移交元素;

(2)SynchronousQueue有两种实现方式,一种是公平(队列)方式,一种是非公平(栈)方式;

(3)栈方式中的节点有三种模式:生产者、消费者、正在匹配中;

(4)栈方式的大致思路是如果栈顶元素跟自己一样的模式就入栈并等待被匹配,否则就匹配,匹配到了就返回;

(5)队列方式的大致思路是……(两者的逻辑差别还是挺大的)

彩蛋

(1)SynchronousQueue真的是无缓冲的队列吗?

通过源码分析,我们可以发现其实SynchronousQueue内部或者使用栈或者使用队列来存储包含线程和元素值的节点,如果同一个模式的节点过多的话,它们都会存储进来,且都会阻塞着,所以,严格上来说,SynchronousQueue并不能算是一个无缓冲队列。

(2)SynchronousQueue有什么缺点呢?

试想一下,如果有多个生产者,但只有一个消费者,如果消费者处理不过来,是不是生产者都会阻塞起来?反之亦然。

这是一件很危险的事,所以,SynchronousQueue一般用于生产、消费的速度大致相当的情况,这样才不会导致系统中过多的线程处于阻塞状态。


参考地址:https://www.cnblogs.com/tong-yuan/p/SynchronousQueue.html

参考地址:https://www.battleheart.cn/2019/05/18/synchronousqueue-principle-unfair-pattern/

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值