并发容器
Java并发容器JUC是三个单词的缩写。是JDK下面的一个包名。即Java.util.concurrency
。
CopyOnWriteArrayList
CopyOnWriteArrayList 写操作时复制,当有新元素添加到集合中时,从原有的数组中拷贝一份出来,然后在新的数组上作写操作,将原来的数组指向新的数组。整个数组的add操作都是在锁的保护下进行的,防止并发时复制多份副本。读操作是在原数组中进行,不需要加锁。
源码分析
下面首先展示了CopyOnWriteArrayList中比较重要的成员:
1 | /** The lock protecting all mutators */ |
CopyOnWriteArrayList使用了ReentrantLock来支持并发操作,array就是实际存放数据的数组对象。ReentrantLock是一种支持重入的独占锁,任意时刻只允许一个线程获得锁。下面首先展示了add方法的代码:
1 |
|
现在来分析一下读操作,下面是get方法的细节:
1 |
|
可以发现是非常简单的,而且读是允许多个线程进入的。
下面来分析一下CopyOnWriteArrayList的迭代器。下面是两个重要的变量:
1 |
|
遍历的时候首先会获得当前数组对象的一个拷贝,称为快照,然后遍历的操作会在该快照上进行。获取了迭代器之后再对CopyOnWriteArrayList进行写操作会,迭代器不会感知到这种变化。每一个线程都将获得当前时刻的一个快照,所以不需要加锁就可以安全的实现遍历。
设计思想
- 读写分离
- 最终一致性
- 使用时另外开辟空间,防止并发冲突
缺点
- 写操作时复制消耗内存,如果元素比较多时候,容易导致young gc 和full gc。
- 不能用于实时读的场景。由于复制和add操作等需要时间,故读取时可能读到旧值。能做到最终一致性,但无法满足实时性的要求,更适合读多写少的场景。如果无法知道数组有多大,或者add,set操作有多少,慎用此类,在大量的复制副本的过程中很容易出错。
ConcurrentHashMap
Map 这样的 Key Value
在软件开发中是非常经典的结构,常用于在内存中存放数据。
HashMap
众所周知 HashMap 底层是基于 数组 + 链表
组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。
Base 1.7
Base 1.8
其实一个很明显的地方就是:当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)
。因此 1.8 中重点优化了这个查询效率,判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。
线程不安全
HashMap 扩容的时候会调用 resize()
方法,就是这里的并发操作容易在一个桶上形成环形链表;这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就会出现死循环。
ConcurrentHashMap
Base 1.7
如图所示,是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。
它的核心成员变量:
1 | /** |
1 | static final class Segment<K,V> extends ReentrantLock implements Serializable { |
原理上来说:ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。
Base 1.8
1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但查询遍历链表效率太低。1.8抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized
来保证并发安全性。
ConcurrentSkipListMap
底层实现采用SkipList跳表
曾经有人用ConcurrentHashMap与ConcurrentSkipListMap做性能测试,在4个线程1.6W的数据条件下,前者的数据存取速度是后者的4倍左右。但是后者有几个前者不能比拟的优点:
- Key是有序的
- 支持更高的并发,存储时间与线程数无关
安全共享对象策略:
- 线程限制:一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改
- 共享只读:一个共享只读的U帝乡,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改它
- 线程安全对象:一个线程安全的对象或者容器,在内部通过同步机制来保障线程安全,多以其他线程无需额外的同步就可以通过公共接口随意访问他
- 被守护对象:被守护对象只能通过获取特定的锁来访问。