赞
踩
CopyOnWriteArrayList/CopyOnWriteArraySet:
——几乎不更新,通常只做遍历
Copy-On-Write简称COW,字面意思为修改内容时进行拷贝。这是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。
然而,由于数组的拷贝开销很大,因此这种策略在需要对集合元素进行频繁更新时
实现细节:
CopyOnWriteArrayList以数组实现,保存了一个transient final ReentrantLock lock的锁,用于在对元素进行写入操作时进行加锁。CopyOnWriteArraySet则是以CopyOnWriteArrayList为基础,其保存了CopyOnWriteArrayList的一个引用,所有操作与CopyOnWriteArrayList类似,只是新增时要检查集合中是否有相同元素。
因为每次写集合时都会创建新副本,所以并不会遇到扩容问题。但是,向集合中添加元素时最好按批次添加,如果每一条新数据添加一次,每次都会复制集合副本,开销过大。
此外,CopyOnWrite机制还解决了其他集合由于快速失败机制带来的问题:在一个线程遍历集合时,如果另一个线程向集合中添加或删除了元素,会抛出ConcurrentModificationException。CopyOnWrite机制允许在遍历集合时向集合中增删元素,因为两者操作的不是一个集合对象。
使用场景:
CopyOnWriteArrayList/CopyOnWriteArraySet尤其适用于极少更新,但是存在大量查询操作的场景。
缺点:(引用自点击打开链接)
内存占用问题:
因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。
数据一致性问题:
CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
性能比价:(引用自:点击打开链接)
CopyOnWriteArrayList:专为多线程并发设计的容器,“写入时复制”策略。
Collections.synchronizedMap:同步容器,独占策略。
结果:
线程数量 | 平均访问时间 | 平均访问时间 |
CopyOnWriteArrayList | Collections.synchronizedMap | |
2 | 20 | 100 |
4 | 25 | 500 |
8 | 50 | 2200 |
16 | 120 | 8000 |
32 | 200 | 30000 |
64 | 550 | 120000 |
分析:
可以看到随着线程数不断翻倍,CopyOnWriteArrayList的访问时间基本也是翻倍,但Collections.synchronizedMap的时间则是*4。在两个线程下Collections.synchronizedMap访问时间大概是CopyOnWriteArrayList的5倍,但在64线程的时候就变成了200倍+。所以如果在容器完全只读的情况下CopyOnWriteArrayList绝对是首选。但CopyOnWriteArrayList采用“写入时复制”策略,对容器的写操作将导致的容器中基本数组的复制,性能开销较大。所以但在有写操作的情况下,CopyOnWriteArrayList性能不佳,而且如果容器容量较大的话容易造成溢出。代码中如果CopyOnWriteArrayList cl按照ArrayList al的方法初始化就会造成溢出。
Collections.synchronizedMap、Collections.synchronizedSortedMap、ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListMap、TreeMap
- static final class HashEntry<K,V> {
- final int hash;
- final K key;
- volatile V value;
- volatile HashEntry<K,V> next;
-
- HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
- this.hash = hash;
- this.key = key;
- this.value = value;
- this.next = next;
- }
-
- /**
- * Sets next field with volatile write semantics. (See above
- * about use of putOrderedObject.)
- */
- final void setNext(HashEntry<K,V> n) {
- UNSAFE.putOrderedObject(this, nextOffset, n);
- } }
注意,HashEntry只提供了setNext方法,所以所有节点的修改只能从头部开始。
ArrayBlockingQueue
(带边界的阻塞式队列):
ArrayBlockingQueue是以数组为基础实现的带有边界的阻塞式队列,边界指的是队列容量是固定的,而且容量必须人为指定,无默认值,且无法扩容。阻塞指的是,如果队列满了,向队列中添加元素的操作会阻塞,直到队列中有空位产生,同样,如果队列为空,向队列请求元素的操作也会阻塞,直到队列中有元素可以取。
三个存入方法:add,offer,put
空间耗尽时offer()函数不会等待,直接返回false,add会抛出异常IllegalStateException("Queue full"),而put()则会wait。
三个取出方法:take,poll
使用take()函数,如果队列中没有数据,则线程wait释放CPU,而poll()可以等待time参数规定的时间,到时仍取不到,返回null。在取出元素时,会将数组中的所有元素向前移。
LinkedBlockingDeque / LinkedBlockingQueue(链表队列(带锁),可设定是否带边界)
带边界,线程阻塞-安全的。可以指定容量,如果不指定,默认Integer.MAX_VALUE,这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
ArrayBlockingQueue和LinkedBlockingQueue的区别:
1. 队列中锁的实现不同
ArrayBlockingQueue实现的队列中的锁是没有分离的,存取元素用的是同一个锁;
LinkedBlockingQueue实现的队列中的锁是分离的,即存入用putLock,取出用takeLock
2. 在生产或消费时操作不同
ArrayBlockingQueue实现的队列中在生产和消费的时候,是直接将枚举对象插入或移除的;
LinkedBlockingQueue实现的队列中在生产和消费的时候,需要把枚举对象转换为Node<E>进行插入或移除,会影响性能
3. 队列大小初始化方式不同
ArrayBlockingQueue实现的队列中必须指定队列的大小;
LinkedBlockingQueue实现的队列中可以不指定队列的大小,但是默认是Integer.MAX_VALUE
注意:
1. 在使用LinkedBlockingQueue时,若用默认大小且当生产速度大于消费速度时候,有可能会内存溢出
2. 在使用ArrayBlockingQueue和LinkedBlockingQueue分别对1000000个简单字符做入队操作时,
LinkedBlockingQueue的消耗是ArrayBlockingQueue消耗的10倍左右,
即LinkedBlockingQueue消耗在1500毫秒左右,而ArrayBlockingQueue只需150毫秒左右。
BlockingQueue不接收空值
ConcurrentLinkedDeque / ConcurrentLinkedQueue:(无边界的链表队列(CAS))
ConcurrentLinkedQueue的size方法会遍历集合,所以开销大。如果要判断是否有元素,使用isEmpty方法。
PriorityBlockingQueue:
类似于LinkedBlockQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数的Comparator决定的顺序.
SynchronousQueue:
特殊的BlockingQueue,对其的操作必须是放和取交替完成的.
DelayQueue
——元素带有延迟的队列
LinkedTransferQueue
——可将元素`transfer`进行w/o存储
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。