Java队列总结

720 阅读8分钟

文章大纲
  • Java的Queue接口

  • 影响队列性能的因素

  • 数组和链表

  • 锁和CAS

  • 非线程安全队列

  • “渣猪”队列

  • 线程安全队列-非阻塞队列

  • 线程安全队列-阻塞队列

  • JCTools包中的队列

  • Disruptor队列

  • 相关资料

  • 速记卡-队列方法

  • 速记卡-队列分类





最近对JAVA的队列比较感兴趣,今天就来探讨下。

有句话怎么说来着, 一流企业定标准、二流企业做品牌、三流企业卖技术、四流企业做产品。

所以了解队列当然就是先从接口开始啦。



我们从接口的注释上可以清楚明白地看到Queue接口的各个方法的差异。


我们可以对队列执行增,删,查操作,当队列满了的时候,通过add新增元素就会抛异常,通过offer新增元素失败就会返回false,其实add方法的内部实现就是调用了offer方法。

了解了Queue接口后,接下来我们关心的就是各种队列的具体实现了。开始之前先来说说影响队列性能的一些关键因素。


1 数组和链表

队列的底层结构只有两种, 数组Array, 链表Linked。

但是呢,数组是比链表快的,因为数组内元素的内存地址是连续的。而且数组的容量是有限的, 相对来说发生内存溢出的风险会小很多。

实现一个队列,选择存储结构的优先级是:Linked < Array。


2 锁和CAS

我们先介绍下锁,后面再谈谈为什么要用到锁。


synchronized, ReentrantLock, 是Java中的悲观锁。

CAS,是Compare And Swap的缩写,顾名思义就“比较并替换”,是一种乐观锁实现。


下面用Benchmark在Java8环境下测试一下悲观锁,CAS和无锁时的性能。我们用两个线程对一个long类型的字段进行递增操作。

@BenchmarkMode({Mode.SampleTime}) // 测试方法平均执行时间
@OutputTimeUnit(TimeUnit.MILLISECONDS) // 输出结果的时间粒度为微秒
@Warmup(iterations = 3, time = 5, timeUnit = TimeUnit.MILLISECONDS) // 预热次数
@Measurement(iterations = 1, batchSize = 1000) // 测试次数
@Threads(2)
@Fork(1)
@State(Scope.Benchmark)
public class LockTest {
​
    private java.util.concurrent.locks.Lock lock = new ReentrantLock();
    private long lockIndex = 0;
    private long noLockIndex = 0;
    private long synIndex = 0;
    private AtomicLong atomicLong = new AtomicLong(0);
​
    @Benchmark
    public void measureLock() {
​
        lock.lock();
        lockIndex++;
        lock.unlock();
    }
​
    @Benchmark
    public void measureSyn() {
        synchronized (this) {
            synIndex++;
        }
    }
    @Benchmark
    public void measureCAS() {
        atomicLong.incrementAndGet();
    }
​
    @Benchmark
    public void measureNoLock() {
        noLockIndex++;
    }
    
    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(LockTest.class.getSimpleName())
                .build();
        new Runner(opt).run();
    }
}

下面是测试的结果,虽然测试不是很严谨,但是依旧可以看到,无锁时的性能是最好的,其次是CAS,最后才是悲观锁。

Benchmark                    Score    Error  Units
LockTest.measureNoLock       0.003 ±  0.001  ms/op
LockTest.measureCAS          0.038 ±  0.001  ms/op
LockTest.measureSyn          0.086 ±  0.001  ms/op
LockTest.measureLock         0.178 ±  0.001  ms/op

实现一个队列,选择锁技术的优先级是:ReentrantLock < synchronized < CAS < 无锁。

经过上面两点总结,我们发现:要想实现一个性能好的队列,最好是基于数组的,并且最好是无锁的。

那么我们就先去Java中找找有没有采用“数组+无锁”方案的队列。


3 非线程安全队列

遗憾的是,我们没有找到“数组+无锁”方案的队列。

这里要说说我们常用的ArrayList为什么不能做队列呢?
因为ArrayList是一个动态数组,是可以扩容的。如果用做队列, 无论是入队时在队头插入数据,还是出队时在队尾删除数据,都涉及到数据迁移,这性能就会非常低下。而我们后面要说的ArrayBlockingQueue,他的数组容量是不可变的!

当然,优先队列PriorityQueue是基于堆,并且也是无锁的队列,堆的底层也是数组实现,但是这种只能用于特定场景的队列这里先不做讨论。

基于上面的情况,我们退而求其次,找找采用“链表+无锁”方案的队列。

LinkedList就是基于链表,并且无锁的双端队列。双端队列不仅队头可以插入元素,队尾也可以插入元素。

见下图,P表示一个生产者,C表示消费者。一个链表队列会维护一个头指针和一个尾指针,头指针指向队头的元素,尾指针指向队尾的元素。请注意,生产者只有持有了头指针,才可以在队头添加元素;消费者持有了尾指针才可以消费队尾的元素。因为不管是添加还是删除元素,最后都要更新对应的指针。

然而,无锁的队列虽然是快了,但是却无法支撑多人运动呐。

无锁,也就代表着非线程安全。无锁的队列在同一时间只能支持一个生产者线程或者一个消费者线程。

当出现多个生产者同时往一个队列放入元素,或者多个消费者同时从一个队列中取出元素时,就会导致多个线程争抢头指针或者尾指针。


4“渣猪”队列

提起多人运动,情不自禁地就想到了“渣猪”。

如果把“渣猪”看作一个队列,他到底是如何在这么多妹子线程的轰炸中存活下来的呢?

为了弄清这个学术问题,我默默地打开了他前女友的微博细细研读起来,真是越看越心惊。




我们对“渣猪”的这种行为是表示谴责的,但是 “渣猪” 这种队列绝对是我们梦寐以求的高性能队列。

首先,他生活的一切都处理的井井有条,聊妹,跳舞,上节目。这还不是重点,重点是,并发聊妹,多人运动,对多线程场景的并发处理能力堪称惊人!这妥妥的高并发属性啊。

半夜三四点刚给你道晚安,凌晨五点就可以起来发声明,也即是说不管什么时候找他,都能给你响应,这妥妥的高可用属性啊!

一天24小时都能完美地利用起来,也即是说,如果给他一个性能100分机器, “渣猪”能给你跑出110分的效果来。这简直就是高性能框架的典范。


哎呀,话题扯远了!

“沐浴露还有,迪丽热巴也依旧爱着我”,队列怎么才能支持多人运动呢?

我们需要的是线程安全的队列。


5 线程安全队列


线程安全的队列可以分为阻塞队列和非阻塞队列。

用到悲观锁技术,如synchronized ,ReentrantLock的就是阻塞队列。

而使用 CAS 的就是非阻塞队列。


非阻塞队列

我们优先考虑的方案当然是最快最强的“数组+CAS”。然而,现实再次打脸,我们没有在Java并发包下找到这种队列。


那么有没有基于“链表+CAS”方案的队列呢?幸运的是,ConcurrentLinkedQueue队列正是基于这个方案实现,采用了Michael-Scott算法。

其实基于CAS的实现方案都非常复杂,这个就放在下一篇再讲解了。


阻塞队列

要了解阻塞队列,我们还是照旧先看看他的接口。


可以看到, 阻塞队列相对于普通队列,多了个put方法和take方法。从接口的注释上面,我们可以看到这几个新方法的特点。


put方法和take方法的特点就是阻塞了,这也是阻塞队列的特点。

当队列是空时,消费者线程想从队列中读取元素会被阻塞;当队列是满时,生产者想往队列中插入元素也会被阻塞。


原有的offer方法和poll方法,也可以支持设置超时时间了。

时间不早了,阻塞队列就先介绍到这,下一篇我们再一起探讨下阻塞队列ArrayBlockingQueue和LinkedBlockingQueue之间的故事。


6 后续

话又说回来,虽然上面已经有很多精心设计的优秀队列。但是我还是对传说中基于“数组+CAS”方案实现的队列念念不忘,最好是还能支持多个消费者和多个生产者同时操作。

人性如此!但是,还真的有人开发出了这种高性能的队列。

Netty中引入的JCTools,就是一款对jdk并发数据结构进行增强的并发工具。其中对队列使用的各种场景都有考虑到:

SPSC:单个生产者对单个消费者(无等待、有界和无界都有实现)
MPSC:多个生产者对单个消费者(无锁、有界和无界都有实现)
SPMC:单生产者对多个消费者(无锁、有界)
MPMC:多生产者对多个消费者(无锁、有界)
比如里面的MpscArrayQueue队列,就是继承了抽象类ConcurrentCircularArrayQueue,底层就是基于环形数组+CAS实现。

还有优秀的高性能框架disruptor,他们设计出了新的数据结构Ringbuffer,对环形数组进行了优化,从双指针改为单指针。同时disruptor还参考了CPU的内存屏障指令,其实我们Java中的关键字volatile也用到了读屏障指令和写屏障指令,disruptor对生产者和消费者都加了一道屏障。当然disruptor也有对单线程场景和多线程场景分别提供了不同的策略。

有没有发现,队列的世界,开始变得越来越有趣了。

未完待续。

X References

标题:Java多线程总结之聊一聊Queue - I'm Sure
链接:https://blog.csdn.net/qq_39478853/article/details/78656092

标题:「小马哥技术周报」- 第二十三期《面试虐我千百遍,Java 并发真讨厌》
链接:https://www.bilibili.com/video/av49124110
Github: https://github.com/mercyblitz/tech-weekly

标题:Dissecting the Disruptor: Why it’s so fast (part one) – Locks Are Bad
链接:http://ifeve.com/disruptor-locks-are-bad

标题:JCTools
Github: https://github.com/JCTools/JCTools

标题:disruptor
Github: https://github.com/LMAX-Exchange/disruptor


速记卡

队列方法总结

队列种类总结