写点什么

这些并发容器的坑,你要谨记

  • 2022 年 8 月 29 日
    中国香港
  • 本文字数:2450 字

    阅读完需:约 8 分钟

这些并发容器的坑,你要谨记

本文分享自华为云社区《【高并发】要想学好并发编程,这些并发容器的坑是你必须要注意的!!(建议收藏)》,作者:冰 河 。


其实,在 JDK1.5 之前的线程安全的容器,大多数都是指同步容器,使用同步容器进行并发编程时,最大的问题就是性能很差。因为同步容器中的所有方法都是使用 synchronized 锁进行互斥,串行度太高了,无法真正的做到并行。


所以,在 JDK1.5 之后,JDK 中提供了并发性能更好的容器。JDK1.5 及之后的版本中,提供的线程安全的容器,一般被称为并发容器。

并发容器


与同步容器一样,并发容器在总体上也可以分为四大类,分别为:List、Set、Map 和 Queue。总体上如下图所示。



接下来,我们分别介绍下这些并发容器在使用时的注意事项和避免踩到的坑。

List


并发容器中的 List 相对来说比较简单,就一个 CopyOnWriteArrayList。大家可以从字面的意思中就能够体会到:CopyOnWrite,在写的时候进行复制操作,也就是说在进行写操作时,会将共享变量复制一份。那这样做有什么好处呢?最大的好处就是:读操作可以做到完全无锁化


在 CopyOnWriteArrayList 内部维护了一个数组,成员变量 array 指向这个数组,其核心源代码如下所示。


private transient volatile Object[] array;final Object[] getArray() {	return array;}final void setArray(Object[] a) {	array = a;}
复制代码


当进行操作时,都是基于 array 指向的这个内部数组进行的。例如,我们使用 Iterator 迭代器遍历这个数组时,会按照下图所示的方式进行读操作。



如果在遍历 CopyOnWriteArrayList 时发生写操作,例如,向数组中增加一个元素时,CopyOnWriteArrayList 则会将内部的数组复制一份出来,然后会在新复制出来的数组上添加新的元素,添加完再将 array 指向新的数组,如下图所示。



对于 CopyOnWriteArrayList 的其他写操作和添加元素的操作原理相同,这里就不再赘述了。


使用 CopyOnWriteArrayList 时需要注意的是:


  • CopyOnWriteArrayList 只适合写操作比较少的场景,并且能够容忍读写操作在短时间内的不一致。

  • CopyOnWriteArrayList 的迭代器是只读的,不支持写操作。

Set


对于 Set 接口来说,并发容器中主要有两个实现类,一个是 CopyOnWriteArraySet,另一个是 ConcurrentSkipListSet。其中,CopyOnWriteArraySet 的使用场景、原理与注意事项和 CopyOnWriteArrayList 一致。而 ConcurrentSkipListSet 的使用场景、原理和注意事项和下文的 ConcurrentSkipListMap 一致。这里,我就不再赘述啦。

Map


在并发容器中,Map 接口的实现类主要有 ConcurrentHashMap 和 ConcurrentSkipListMap,而 ConcurrentHashMap 和 ConcurrentSkipListMap 最大的区别就是:ConcurrentHashMap 的 Key 是无序的,而 ConcurrentSkipListMap 的 Key 是有序的。


在使用 ConcurrentHashMap 和 ConcurrentSkipListMap 时,需要注意的是:ConcurrentHashMap 和 ConcurrentSkipListMap 的 Key 和 Value 都不能为空。


这里,我们可以将 Map 相关的类总结成一个表格,如下所示。



这样,大家记忆起来就方便多了。


这里,ConcurrentSkipListMap 是基于“跳表”实现的,跳表的插入、删除、查询的平均时间复杂度为 O(log n),这些时间复杂度在理论上与线程数没有关系。如果要追求性能的话,可以尝试使用 ConcurrentSkipListMap。

Queue


在 Java 的并发容器中,Queue 相对来说比较复杂。我们先来了解几个概念:


  • 阻塞队列:阻塞一般就是指当队列已满时,入队操作会阻塞;当队列为空时,出队操作就会阻塞。

  • 非阻塞队列:队列的入队和出队操作不会阻塞。

  • 单端队列:队列的入队操作只能在队尾进行,队列的出队操作只能在队首进行。

  • 双端队列:队列的入队操作和出队操作都可以在队首和队尾进行。


我们可以将上述的队列进行组合,将队列分为单端阻塞队列、双端阻塞队列、单端非阻塞队列和双端非阻塞队列。



在 Java 的并发容器中,会使用明显的标识来区分不同类型的队列。


  • 阻塞队列一个明显的标识就是使用 Blocking 修饰,例如,ArrayBlockingQueue 和 LinkedBlockingQueue 都是阻塞队列。

  • 单端队列会使用 Queue 标识,例如 ArrayBlockingQueue 和 LinkedBlockingQueue 也是单端队列。

  • 双端队列会使用 Deque 标识,例如 LinkedBlockingDeque 和 ConcurrentLinkedDeque 都是双端队列。


接下来,我们就分别简单聊聊这四种类型的队列。

单端阻塞队列


在 Java 的并发容器中,单端阻塞队列的主要实现是 BlockingQueue,主要包括:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、LinkedTransferQueue、PriorityBlockingQueue 和 DelayQueue。



单端阻塞队列的内部一般会有一个队列。


在实现上,内部的队列可以是数组,例如 ArrayBlockingQueue,也可以是链表,例如 LinkedBlockingQueue。


也可以在内部不存在队列,例如 SynchronousQueue,SynchronousQueue 实现了生产者的入队操作必须等待消费者的出队操作完成之后才能进行。


LinkedTransferQueue 集成了 LinkedBlockingQueue 和 SynchronousQueue 的优点,并且性能比 LinkedBlockingQueue 好。


PriorityBlockingQueue 实现了按照优先级进行出队操作,也就是说,队列元素在 PriorityBlockingQueue 内部可以按照某种规则进行排序。


DelayQueue 是延时队列,实现了在一段时间后再出队的操作。

双端阻塞队列


双端阻塞队列的实现主要是 LinkedBlockingDeque。示意图如下所示。


单端非阻塞队列


单端非阻塞队列的实现主要是 ConcurrentLinkedQueue,示意图如下所示。


双端非阻塞队列


双端非阻塞队列的实现主要是 ConcurrentLinkedDeque,示意图如下所示。


有界与无界队列


使用队列时,还要注意队列的有界与无界问题,也就是在使用队列时,需要注意队列是否有容量限制

在实际工作中,一般推荐使用有界队列。因为无界队列很容易导致内存溢出的问题。在 Java 的并发容器中,只有 ArrayBlockingQueue 和 LinkedBlockingQueue 支持有界,其他的队列都是无界队列。


在使用时,一定要注意内存溢出问题。

总结


今天我们主要介绍了 JDK1.5 之后提供的并发容器,主要包括:List、Set、Map 和 Queue,而 Queue 又可以分为:单端阻塞队列、双端阻塞队列、单端非阻塞队列和双端非阻塞队列。对于每种并发容器,我们简单介绍了其基本原理和注意事项。


点击关注,第一时间了解华为云新鲜技术~

发布于: 刚刚阅读数: 4
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
这些并发容器的坑,你要谨记_后端_华为云开发者联盟_InfoQ写作社区