并发队列常见于生产者消费者的场景,例如log4j2,logback的异步日志,例如类似于链路日志的收集上送,以上二者之所以要使用并发队列的很大原因都是因为日志异步化处理,避免影响业务接口的吞吐量。 

  当程序引入了异步队列这个机制,就需要考虑到一些问题,比如如何控制队列的长度,是否会带来额外的内存负担,队列满了的策略:是阻塞业务线程还是丢弃,机器突然宕机了,队列里的数据怎么办?有好处,也有坏处,需要所谓的trade-off,不论是log4j2或者是logback,异步Appender并不是默认选项,大多数应用不需要考虑异步化日志,除非你的应用真正到了需要异步化打印日志来提高吞吐量的地步了,可以看看log42官网关于异步appender的介绍,或者可以看一下logback的AsyncAppender源码,是一个典型的异步队列使用场景,使用了JDK的ArrayBlockingQueue,log4j2则引入了第三方框架Disruptor

  回到队列本身,并发队列,最大的要解决的问题控制并发下的正确性,通常有两种: 1.加锁  2.CAS(lock-free),前者通常就是常见的BlockingQueue,比如JDK的ArrayBlockingQueue以及LinkedBlockingQueue,后者JDK提供了ConcurrentLinkedQueue这个类。以及用的比较多的第三方框架:JCTools

  -------------

  BlockingQueue 以及 JUC包下ArrayBlockingQueue以及LinkedBlockingQueue其异同,以及使用场景。

  BlockingQueue,阻塞队列,基于锁实现,生产者,或者是消费者线程在并发竞争时可能会拿不到锁,被挂起进入BLOCK状态,这个代价是比较"昂贵"(真有这么贵吗。。不至于不至于,大部分场景应该都没事)的,会影响队列的吞吐量,如何最大程度减少锁竞争是必要的。

  LinkedBlockingQueue使用了哪些机制来减少锁竞争?

  1.使用了"two lock queue" algorithm,该算法的主要作用是生产者和消费者之间不会相互阻塞,竞争的是不同的锁,两把锁,两个条件变量,借助AtomicInteger维护count变量(因为count变量可能会被生产者消费者线程同时更新);

  2.cascading notifies:级联通知。

  以上两点查看源代码就可以有一个比较清晰的认识,生产者进行enqueue,入队操作,消费者进行dequeue,出队操作,前者操作尾节点,后者操作头节点,所以可以用两把锁去控制,操作不同的节点,这样生产者消费者之间不会有竞争,唯一一个生产者消费者都要更新的变量:count,使用原子类去维护,确保其线程安全性。

  而对于级联通知,即:When a put notices that it has enabled at least one take, it signals taker. That taker in turn signals others if more items have been entered since the signal.(摘自注释),这样子最大程度避免线程的唤醒再竞争,也就是避免了锁的竞争,看一下put的代码(省略了一些),加了相关注释:

// 队列满,wait
            while (count.get() == capacity) {
                notFull.await();
            }
            // 被唤醒了,入队 
            enqueue(node);
            if (c + 1 < capacity)
                // 如果发现生产了之后,队列还是没满,那么继续通知别的生产者来消费
                notFull.signal();
putLock.unlock(); 
            if (c == 0)      
              // 如果发现,生产之前,队列是空的,那么通知消费者来消费 
              // 就好比说:我现在已经生产了一个了,你们可以来消费了。(此时生产者肯定是wait状态)    
              signalNotEmpty();

   ArrayBlockingQueue呢?ArrayBlockingQueue底层使用的是数组,这意味着它必定是有界的,并且是一个会循环利用的数组(RingBuffer),维护了两个Index:putIndex和takeIndex,类似于LBQ的head和tail节点,但是,它没有使用LBQ的双锁算法,这个我认为是令人比较困惑的,全局使用了一把锁,也就是说生产者和消费者之间是会互相阻塞的,所以可以看到ABQ的count变量就是一个int变量,因为对于count的更新都是在同步代码块中。

       为什么不使用两把锁?两把锁的实现的LBQ的吞吐量是高于ABQ的。有人认为LBQ头尾节点是两个单独的节点,所以可以分开锁,而ABQ底层是一个数组,所以必须是一把锁,但是其实数组的头尾依旧可以使用两把锁去控制,可以做一个简单的实现并且做一个简单的吞吐量测试,测试代码使用的是<<Java并发编程实践>>书上使用的代码,在我的电脑上测试,双锁的ABQ确实有更好的吞吐量?完整代码,包括吞吐量测试的代码:https://pastebin.com/QpW9dsVc 。   

     注:关于这个问题Stackoverflow上有人问过,但是被接受的答案我理解不了 ,我自己也提了一个,没人回答,但是我觉得不要细究,没有必要。

     这两个阻塞队列的异同以及各自的使用场景。

     1.ABQ是有界的,LBQ可以有界也可以没有,所以如果需要一个无界的队列,那只能选择LBQ。

     2.ABQ底层基于数组,且是预分配的,这意味着实现他就会占据一部分内存,而之后的入队出队则是数组引用的赋值,LBQ则是动态创建节点,这点上看,ABQ显然占优

     3.LBQ双锁,ABQ单锁,吞吐量前者大于后者,这是毋庸置疑的。(<<Java并发编程实践>>上有关于这两个队列的较为详细的介绍,主要在于性能上的对比)

     还值得一提的是,cache伪共享,ABQ和LBQ是没有考虑到的,所谓伪共享就是两个变量被放到同一个缓存行上,改变了其中一个,导致这一行都Invalid了,具体的伪共享自己搜一下

    具体怎么用,选什么,这只有结合自己的使用场景经过一系列基准测试,得出答案,但是我觉得可能真的差不了太多吧,特别是,可能很多测试测出来的结果LBQ吞吐更好,但是这些测试的场景通常是线程除了取数据,或者放数据,没有任何其他操作,也就是take完一个元素,立马接着take下一个(put同理),也就是大量线程可能同时去竞争锁,而真实的场景一般不是这样的,不论是生产或者消费,拿到数据之后或者生成数据之前都会有一系列其他操作,所以可能差距会被放小。

    下面的观点摘自公开邮件,首先是Brian Goetz(Oracle的架构师),他的观点是:

In most cases, allocation is dirt cheap -- certainly cheaper than contention -- so in most cases LBQ is preferable. In RT environments, where memory is more constrained and GC pauses are less acceptable, ABQ may be more appropriate.

    Doug lea的观点:


Usually, when you are putting something into a queue, you will have just allocated that new something. And similarly, when you take something out you usually use it and then let it become garbage. In which case the extra allocation for a queue node is not going to make much difference in overall GC, so you might as well go for the better scalability of LinkedBlockingQueue. I think this is the most common use case.

But, if you aren't allocating the things put into queues, and don't expect lots of threads to be contending when the queue is neither empty nor full, then ArrayBlockingQueue is likely to work better.


  简而言之,前者认为,动态分配节点不是什么事,吞吐量比较重要,而在RT(响应时间)环境中,ABQ可能更加可以接受,因为正如上面的第二点所说,预分配内存,入队出队只是引用的赋值,肯定会快于LBQ,且更加GC-friendly,而Doug lea大佬的回答就比较抽象了,这是我的个人理解:他认为如果你是实时创建一些对象,扔到队列里,那肯定是LBQ好,因为LBQ的缺点就是动态创建NODE,既然你自己都会创建,那么直接用它也没差别了,他又拥有更好的吞吐,而如果你扔到队列里的是一些以前就存在的(long-lived)对象,那ABQ更好。而对于这段话:

don't expect lots of threads to be contending when the queue is neither empty nor full, then ArrayBlockingQueue is likely to work better. 这样去理解:假设队列是空的,或者满的,此时有两个线程,一个取,一个放,那么ABQ会导致这两者相互阻塞(single-lock),LBQ则不会,所以如果队列大多时候是既不是满,也不是空的,那么ABQ更好。

 

  以上是关于阻塞队列的讨论,接下来讨论一下lock-free的队列,Disruptor的文章有描述一些性能上的差距,锁的劣势是线程的挂起和恢复是存在比较大的开销的,甚至大于程序本身,JDK之前版本的synchronized性能是比较差的,而且现在的JVM会有一些优化,比如当线程发现锁被占用时,不一定立马将自己挂起来,可能会自旋几次(这取决于之前这个线程对于锁的持有长短),一旦自旋之后都获取不到,那么就会到了操作系统这一层,将自己挂起来。 而现如今硬件是支持CAS这种乐观的机制的,JUC下也提供了一系列原子类,就是基于CAS实现的,通常来说,竞争很激烈的情况下,锁的吞吐可能会更好,因为大部分CAS可能都是在空转,竞争中等的情况下,CAS的优势就比较明显了。

    回到队列本身,无锁的队列,JUC下是ConcurrentLinkedQueue,以及Disruptor或者JCTools,JCTools提供了一系列面向不同场景的队列,例如MPMC(多生产者多消费者),MPSC等。真正想要提高性能,不同场景下设计专门的队列是可以提高很多的,比如最简单的SPSC,一个就是两个线程,一个操作put,一个操作take,很多地方可以避免上锁,甚至无锁的CAS都不需要了。Netty的NioEventLoop用于存储任务的队列使用的就是JCTools的MPSC,NioEventLoop中轮询线程很多时候是hang在selector上等待IO事件的,并不是一直在轮询任务队列。 Netty的时间轮工具类也用了MPSC。通常来说,生产着消费者场景,如果队列没数据,其实就是应该block住吧?无锁队列下,消费者可以基于一定策略等待,比如sleep 50毫秒,再去poll一下,但是极端情况下肯定会存在50ms的延迟,例如Disruptor就提供了一系列wait策略,block或者sleep等等。

 

上述很多只是我自己浅薄的理解 ,可能有很多地方经不起推敲&有错误的

 

   EDIT:

 

   对于MPSC,看到一个很巧妙的无锁实现,NODE本身作为一个AtomicReference,set成后置节点,类似于next,这样的好处是,直接使用:head.getAndSet(node).lazySet(node);,这个操作是不存在线程安全问题的,着实巧妙,但是性能请自己测试,我测了之后看起来比不过JCTOOLS这些成熟的库:

 

public final class MPSCQueue<E> implements Queue<E> {
    Node<E> tail = new Node<E>(null);
    final AtomicReference<Node<E>> head = new AtomicReference<Node<E>>(tail);

    @Override
    public boolean offer(E e) {
        Node<E> node = new Node<E>(e);
        head.getAndSet(node).lazySet(node);
        return true;
    }

    @Override
    public E poll() {
        Node<E> next = tail.get();
        if (next == null) {
            return null;
        } else {
            tail = next;
            return next.value;
        }
    }

    static class Node<T> extends AtomicReference<Node<T>> {

        final T value;

        Node(T value) {
            this.value = value;
        }
    }
}