一、并发容器简介
| 并发容器 | 对应的普通容器 | 描述 |
|---|---|---|
ConcurrentHashMap | HashMap | Java 1.8 之前采用分段锁机制细化锁粒度,降低阻塞,从而提高并发性;Java 1.8 之后基于 CAS 实现。 |
ConcurrentSkipListMap | SortedMap | 基于跳表实现的 |
CopyOnWriteArrayList | ArrayList | |
CopyOnWriteArraySet | Set | 基于 CopyOnWriteArrayList 实现。 |
ConcurrentSkipListSet | SortedSet | 基于 ConcurrentSkipListMap 实现。 |
ConcurrentLinkedQueue | Queue | 线程安全的无界队列。底层采用单链表。支持 FIFO。 |
ConcurrentLinkedDeque | Deque | 线程安全的无界双端队列。底层采用双向链表。支持 FIFO 和 FILO。 |
ArrayBlockingQueue | Queue | 数组实现的阻塞队列。 |
LinkedBlockingQueue | Queue | 链表实现的阻塞队列。 |
LinkedBlockingDeque | Deque | 双向链表实现的双端阻塞队列。 |
Concurrent*- 这类型的锁竞争相对于
CopyOnWrite*要高一些,但写操作代价要小一些。 - 此外,
Concurrent*往往提供了较低的遍历一致性,即:当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历。代价就是,在获取容器大小size(),容器是否为空等方法,不一定完全精确,但这是为了获取并发吞吐量的设计取舍,可以理解。与之相比,如果是使用同步容器,就会出现fail-fast问题,即:检测到容器在遍历过程中发生了修改,则抛出ConcurrentModificationException,不再继续遍历。
- 这类型的锁竞争相对于
CopyOnWrite*:一个线程写,多个线程读。读操作时不加锁,写操作时通过在副本上加锁保证并发安全,空间开销较大。Blocking*:内部实现一般是基于锁,提供阻塞队列的能力。
1.1 并发场景下的Map
如果对数据有强一致要求,则需使用 Hashtable;在大部分场景通常都是弱一致性的情况下,使用 ConcurrentHashMap 即可;如果数据量在千万级别,且存在大量增删改操作,则可以考虑使用 ConcurrentSkipListMap
1.2 并发场景下的 List
读多写少用 CopyOnWriteArrayList。
写多读少用 ConcurrentLinkedQueue ,但由于是无界的,要有容量限制,避免无限膨胀,导致内存溢出。
二、 Map
Map 接口的两个实现是 ConcurrentHashMap 和 ConcurrentSkipListMap,它们从应用的角度来看,主要区别在于ConcurrentHashMap 的 key 是无序的,而 ConcurrentSkipListMap 的key是有序的。所以如果你需要保证key的顺序,就只能使用 ConcurrentSkipListMap。
使用 ConcurrentHashMap 和 ConcurrentSkipListMap 需要注意的地方是,它们的key和 value 都不能为空,否则会抛出NullPointerException这个运行时异常.
2.1 ConcurrentHashMap
ConcurrentHashMap 是线程安全的 HashMap ,用于替代 Hashtable。
ConcurrentHashMap 的原理
Java 1.7
- 数据结构:数组+单链表
- 并发机制:采用分段锁机制细化锁粒度,降低阻塞,从而提高并发性。
分段锁,是将内部进行分段(Segment),里面是 HashEntry 数组,和 HashMap 类似,哈希相同的条目也是以链表形式存放。 HashEntry 内部使用 volatile 的 value 字段来保证可见性,也利用了不可变对象的机制,以改进利用 Unsafe 提供的底层能力,比如 volatile access,去直接完成部分操作,以最优化性能,毕竟Unsafe中的很多操作都是 JVM intrinsic 优化过的。
Java 1.8
-
数据结构:数组+单链表+红黑树
-
并发机制:取消分段锁,之后基于 CAS + synchronized 实现。
-
数据结构改进:与 HashMap 一样,将原先
数组+单链表的数据结构,变更为数组+单链表+红黑树的结构。当出现哈希冲突时,数据会存入数组指定桶的单链表,当链表长度达到 8,则将其转换为红黑树结构,长度为6时,又会转换为链表,这样其查询的时间复杂度可以降低到 ,以改进性能(因为链表的查询性能较差,改成红黑树查询效率更高) -
并发机制改进:
- 取消
segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。 - 使用
CAS + sychronized操作,在特定场景进行无锁并发操作。使用Unsafe、LongAdder之类底层手段,进行极端情况的优化。现代 JDK 中,synchronized已经被不断优化,可以不再过分担心性能差异,另外,相比于ReentrantLock,它可以减少内存消耗,这是个非常大的优势。
- 取消
ConcurrentHashMap采用了非常精妙的"分段锁"策略,ConcurrentHashMap的主干是个Segment数组。
Segment继承了ReentrantLock,所以它就是一种可重入锁。在ConcurrentHashMap中,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不同Segment的数据进行操作是不用考虑锁竞争的。所以,对于同一个
Segment的操作才需考虑线程同步,不同的Segment则无需考虑。
Segment类似于HashMap,一个Segment维护着一个HashEntry数组。
HashEntry是目前提到的最小的逻辑处理单元了。一个ConcurrentHashMap维护一个Segment数组,一个Segment维护一个HashEntry数组。
三、List
3.1 CopyOnWriteArrayList
CopyOnWriteArrayList 是线程安全的 ArrayList。CopyOnWrite 字面意思为写的时候会将共享变量新复制一份出来。复制的好处在于读操作是无锁的·(也就是无阻塞)。
CopyOnWriteArrayList 仅适用于写操作非常少的场景,而且能够容忍读写的短暂不一致。如果读写比例均衡或者有大量写操作的话,使用 CopyOnWriteArrayList 的性能会非常糟糕。
CopyOnWriteArrayList 原理
CopyOnWriteArrayList 内部维护了一个数组,成员变量array就指向这个内部数组,所有的读操作都是基于 array 进行的,如下图所示,迭代器 Iterator 遍历的就是 array 数组。
lock- 执行写时复制操作,需要使用可重入锁加锁array- 对象数组,用于存放元素
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
读操作
在 CopyOnWriteAarrayList 中,读操作不同步,因为它们在内部数组的快照上工作,所以多个迭代器可以同时遍历而不会相互阻塞.
CopyOnWriteArrayList 的读操作是不用加锁的,性能很高。
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
写操作
所有的写操作都是同步的。他们在备份数组的副本上工作。写操作完成后,后备阵列将被替换为复制的阵列,并释放锁定。支持数组变得易变,所以替换数组的调用是原子.
写操作后创建的迭代器将能够看到修改的结构。
写时复制集合返回的迭代器不会抛出 ConcurrentModificationException,因为它们在数组的快照上工作,并且无论后续的修改如何,都会像迭代器创建时那样完全返回元素。
添加操作 :添加的逻辑很简单,先将原容器copy一份,然后在新副本上执行写操作,之后再切换引用。当然此过程是要加锁的。
public boolean add(E e) {
//ReentrantLock加锁,保证线程安全
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//拷贝原容器,长度为原容器长度加一
Object[] newElements = Arrays.copyOf(elements, len + 1);
//在新副本上执行添加操作
newElements[len] = e;
//将原容器引用指向新副本
setArray(newElements);
return true;
} finally {
//解锁
lock.unlock();
}
}
删除操作 : 删除操作同理,将除要删除元素之外的其他元素拷贝到新副本中,然后切换引用,将原容器引用指向新副本。同属写操作,需要加锁。
public E remove(int index) {
//加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
//如果要删除的是列表末端数据,拷贝前len-1个数据到新副本上,再切换引用
setArray(Arrays.copyOf(elements, len - 1));
else {
//否则,将除要删除元素之外的其他元素拷贝到新副本中,并切换引用
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
//解锁
lock.unlock();
}
}
转载自:
https://dunwu.github.io/javacore/concurrent/java-concurrent-container.html#_4-1-copyonwritearraylist