Java里的并发队列
并发队列常见于生产者消费者的场景,例如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.
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.
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;
}
}
}

浙公网安备 33010602011771号