当前位置: 首页 > 科技观察

14个Java并发容器,你用过几个?

时间:2023-03-12 09:04:31 科技观察

前言在不考虑多线程并发的情况下,容器类一般使用ArrayList、HashMap等线程不安全类,效率更高。在并发场景中,经常会用到ConcurrentHashMap、ArrayBlockingQueue等线程安全的容器类。虽然牺牲了一些效率,但获得了安全性。上面提到的线程安全容器都在java.util.concurrent包下。这个包下有很多并发容器,所以今天就挖出来鼓捣一下。简单介绍一下,后面我们会深入探讨。并发容器介绍ConcurrentHashMap:HashMap的并发版本CopyOnWriteArrayList:ArrayList的并发版本CopyOnWriteArraySet:ConcurrentSetConcurrentLinkedQueue:并发队列(基于链表)ConcurrentLinkedDeque:并发队列(基于双向链表)ConcurrentSkipListMap:基于跳表的ConcurrentMapConcurrentSkipListSet:ConcurrentSetbasedonskiplistArrayBlockingQueue:阻塞队列(基于数组)LinkedBlockingQueue:阻塞队列(基于链表)LinkedBlockingDeque:阻塞队列(基于双向链表)PriorityBlockingQueue:线程安全优先级队列SynchronousQueue:读写配对队列LinkedTransferQueue:基于链表的数据交换队列DelayQueue:延迟队列1.ConcurrentHashMap并发版本HashMap是最常见的并发容器之一,在并发场景下可以作为缓存使用。底层依然是哈希表,但是JAVA8有了很大的变化,而且JAVA7和JAVA8都有更多的版本使用,所以经常比较这两个版本的实现方式(比如面试).一个比较大的区别是在JAVA7中,使用了分段锁来减少锁竞争。JAVA8中放弃了分段锁,采用了CAS(一种乐观锁)。同时,为了防止哈希值严重冲突退化为链表(发生冲突时,会在该位置生成一个链表,将哈希值相同的对象链接在一起),并将链表长度达到阈值(8)后转化为红黑树(相比链表,树的查询效率更稳定)。2.CopyOnWriteArrayList并发版的ArrayList并发版的ArrayList,底层结构也是一个数组,与ArrayList的区别是在添加或删除元素时会创建一个新数组,在新数组中添加或排除指定对象,以及finally使用新的增量数组替换原来的数组。适用场景:由于读操作不加锁,写(增、删、改)操作加锁,适用于读多写少的场景。局限性:由于read不会加锁(读效率高,和普通ArrayList一样),read的当前副本可能读到脏数据。如果介意,建议不要。看下源码感受下:3.CopyOnWriteArraySetConcurrentSet是基于CopyOnWriteArrayList实现的(包含一个CopyOnWriteArrayList成员变量),也就是说最底层是一个数组,也就是说每次add都需要遍历整个集合以了解它是否存在。如果不存在,则需要插入(锁定)。适用场景:CopyOnWriteArrayList的适用场景加一个,设置不要太大(遍历都伤不起)。4.ConcurrentLinkedQueue并发队列(基于链表)基于链表的并发队列,使用乐观锁(CAS)保证线程安全。因为数据结构是链表,理论上对队列的大小没有限制,也就是说添加数据一定要成功。5、ConcurrentLinkedDeque并发队列(基于双向链表)是基于双向链表实现的并发队列,可以对头尾分开操作,所以除了先进先出(FIFO),它也可以是先进后出(FILO)。当然应该叫先进后出吧。6.ConcurrentSkipListMap基于skiplist的ConcurrentMapSkipList是一个skiplist。跳表是一种以空间换时间的数据结构。通过冗余数据对链表进行逐层索引,达到类似二分查找的效果。7、ConcurrentSkipListSet基于skiplist并发Set类似于HashSet和HashMap的关系。ConcurrentSkipListSet是一个ConcurrentSkipListMap,这里就不细说了。8.ArrayBlockingQueue阻塞队列(基于数组)是基于数组实现的可阻塞队列。构造时,必须指定数组的大小。往里面放东西的时候,如果数组满了,就会阻塞,直到有空位为止(也支持直接返回和超时等待),通过一个锁ReentrantLock保证线程安全。乍一看,它可能有点混乱。读和写是同一个锁。如果一个读线程空着就来了,不就一直阻塞吗?答案在于notEmpty和notFull。lock这两个小东西makelock有类似synchronized+wait+notify的功能。传送门→终于明白了sleep/wait/notify/notifyAll9.LinkedBlockingQueueBlockingqueue(basedonlinkedlist)基于链表的阻塞队列,相对于非阻塞的ConcurrentLinkedQueue,多了一个capacitylimit,如果不设置,默认是int最大值。10.LinkedBlockingDeque阻塞队列(基于双向链表)类似于LinkedBlockingQueue,但它提供了双向链表特有的操作。11.PriorityBlockingQueue在构造线程安全的优先级队列时可以传入比较器。可以看出,放入的元素会被排序,然后在读取的时候按顺序消费。一些低优先级的元素可能长时间没有被消费,因为高优先级的元素不断进来。12.SynchronousQueue数据同步交换的队列是假队列,因为它实际上没有真正的空间来存储元素,并且每一次插入操作都要有对应的取出操作,不取出就不能继续插入。可以看出写线程没有任何休眠。可以说是在全力投入队列,而读线程很不活跃,读一个然后睡一会。输出的结果是读写操作是成对发生的。JAVA中的一个使用场景是Executors.newCachedThreadPool(),它创建一个缓存线程池。13、LinkedTransferQueue实现了基于链表数据交换队列的接口TransferQueue。通过transfer方法放置元素时,如果发现有线程在获取元素时被阻塞,则直接将元素交给等待线程。如果没有人在等待消费,则该元素被放在队列的末尾,并且此方法阻塞直到有人读取该元素。它有点像SynchronousQueue,但比它更强大。14.DelayQueue延迟队列可以让放入队列的元素在指定的延迟后被消费者取出,元素需要实现Delayed接口。综上所述,以上简单介绍了JAVA并发包下的一些容器类。知道了这些东西,就可以想到一个现成的东西,遇到合适的场景就可以拿来用。想知道为什么,还得在后续进一步探究。作者:追风的Java架构师来源:简书