Java 并发容器
并发容器非常多,自己理解把Java的并发容器常用分为如下三类:
- BlockingQueue系列
ArrayBlockingQueue、LinkedBlockingQueue - Concurent系列
ConcurrentHashMap、ConcurrentLinkedQueue - CopyOnWrite系列
CopyOnWriteArrayList、CopyOnWriteArraySet
分问题整理下自己曾经的疑问。
为什么有这么多并发容器?
这个问题,其实很好回答,场景多么,那一般场景有几个区分维度?维度有下面几个:
- 读多还是写多
- 读是否频繁
- 写是否频繁
- 并发量多大
- 吞吐量的要求
- 是否有批量操作
- 是否所有操作都是原子的
- 更新的实时性要求
- 数据量的大小
其实上述维度并不全,但组合下来算算,java的并发容器种类还好了。
BlockingQueue的feature与场景?
- BlockingQueue is a unique collection type which not only store elements but also supports flow control by introducing blocking
- an ideal choice for implementing Producer consumer design pattern
- BlockingQueue in Java doesn’t allow null elements
- bulk Collection operations like addAll(), containsAll() are not performed atomically
ArrayBlockingQueue VS LinkedBlockingQueue
- ArrayBlockingQueue(浪费) always holds an Object array with full capacity even when empty
- Latency and throughput are 2 different things. ArrayBlockingQueue has better latency since it is faster to set reference in array whereas LinkedBlockingQueue has better throughput since it uses 2 diff locks for put and take and only synchronizes on edge condition.
- Disruptor wipes the floor with both of them
参考:
disruptor
BlockingQueue in Java
ConcurrentLinkedQueue vs LinkedBlockingQueue
首先,最明显的区别就是LinkedBlockingQueue是个block的queue,它实现了BlockingQueue的interface,比如put和take,这些方法是实现生产者消费者模式的必备的接口。如果ConcurrentLinkedQueue需要实现一个阻塞的take,那么需要用poll来等待,是这样的:
while(result == null)
result = concurrentLinkedQueue.poll();
而block的用法是这样的:
linkedBlockingQueue.take();
显然后者在一般的情况下是比较高效的。一般常用的模式就是生产者消费者模式了,那ConcurrentlinkedQueue的应用场景是什么呢?
参考:LinkedBlockingQueue vs ConcurrentLinkedQueue一般来说有两种场景:
- 一是在consumer和producer比较多、consume/produce消息的速率,但具体多少是比较多,肯定是需要benchmark的。
- 二是在一种特殊的情况下,producer一批数据,然后在consumer一批数据,当consumer取不到数据的时候就认为结束了,而不是在继续的poll了。
对于第一种情况,与select和poll模型很像,不要认为select很挫,当IO非常频繁,每次select轮询,绝大多数的socket都有数据的时候,select有可能就比poll高效了。同样一种理想的情况,produce的消息非常快,consume的消息也非常快,而且速度很相近,队列里面有恰好保留那么点数据,这样consume的时候一直有数据,也不至于poll浪费cpu了。
为什么ConcurrentLinkedQueue没有阻塞的put和take方法呢?ConcurrentLinkedQueue是lock-free的结构,如果在加上阻塞的put和take方法,那就不是什么lock-free的了,自然没有了lock-free的优势了。
ConcurrentHashMap的底层实现?为啥能替代Hashtable
- 锁粒度小,冲突少,用分离锁实现多个线程间的共享访问
- HashEntery 对象的不变性来降低读操作对加锁的需求
- 用 Volatile 变量协调读写线程间的内存可见性
CopyOnWrite的feature与场景?
- 可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,读写分离(数据库Redis等),可以用在白名单,黑名单,商品类目的访问和更新场景
- 在拷贝完,修改后,修改指向,这样在修改后,有一段时间读到的数据是旧的数据
- 在读远多于写的场景
ps:与ConcurrentHashMap类似,但是感觉应该用在数据相对较少的场景中(查找效率可能比HashMap高),而且数据量较少复制开销小能避免GC问题
PS:另外,需要用在允许实时性较差的场景中,允许读取一定时间的旧数据
ps:为什么设计成写的时候允许读到旧数据?这样设计为了允许更快速的读取(也是这个数据结构优势所在),如果更新的时候不允许读那么与一般的不copy的结构效率上也没啥差
ps:与ConcurrentHashMap区别,感觉一是在数据量大小上,二是在读取上,CopyOnWrite允许在修改过程中疯狂读取
CopyOnWrite造成的问题
- 内存占用太大时,可能造成频繁的Yong GC和Full GC(Redis木有GC问题)
- 可以使用ConcurrentHashMap代替,但大数据量上遍历也会慢
- 数据一致性问题,可能读到旧数据
为什么没有CopyOnWriteMap和CopyOnWriteLinkedList?
没有CopyOnWriteMap可能因为有了ConcurrentHashMap,也是读远多于写的情况,ConcurrentHashMap已经非常好用了。
为什么没有CopyOnWriteLinkedList没有必要吧,修改LinkedList很快。CopyOnWirte系类的特点的读性能要求高,list的底层结构读效率不如array的。
非线程安全的优势?
就一点,速度快。很多的容器都有自己对应的线程安全版本,如Vector与ArrayList,Hashtable(被ConcurrentHashMap代替)与Hashmap。
PS:HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的
PS:Fail-safe和iterator迭代器相关。如果某个集合对象创建了Iterator或者ListIterator,然后其它的线程试图“结构上(删除或插入)”更改集合对象,将会抛出ConcurrentModificationException异常。 但其它线程可以通过set()方法更改集合对象是允许的,因为这并没有从“结构上”更改集合。但是假如已经从结构上进行了更改,再调用set()方法,将会抛出IllegalArgumentException异常。