Concurrent 并发集合
1、概述
原先使用的 ArrayList、HashMap 、HashSet、等集合都是线程不安全的,因此 java.util.concurrent 包提供了对应的并发集合类
接口 | 非线程安全 | 线程安全 |
---|---|---|
List | ArrayList | CopyOnWriteArrayList |
Map | HashMap | ConcurrentHashMap |
Set | HashSet TreeSet | CopyOnWriteArraySet |
Queue | ArrayDeque LinkedList | ArrayBlockingQueue LinkedBlockingQueue |
Deque | ArrayDeque LinkedList | LinkedBlockingDeque |
这些并发集合与非线程安全的集合类完全相同
2、CopyOnWriteArrayList
(1)基本思想
当我们往一个集合容器中写入元素时(添加、修改、删除),并不会直接在集合容器中写入,而是先将当前集合容器进行Copy,复制出一个新的容器,然后新的容器里写入元素,写入操作完成之后,再将原容器的引用指向新的容器。
(2)数据结构
数组
CopyOnWriteArrayList相当于线程安全的ArrayList,内部存储结构采用Object[]数组,线程安全使用ReentrantLock实现,允许多个线程并发读取,但只能有一个线程写入。
(3)基本方法
1、add()
添加新元素至集合时,会将当前数组复制一个新数组,并将新元素添加至新数组,最后替换原数组】
执行过程中使用 ReentrantLock 加锁,保证线程安全
源码展示:
public boolean add(E e) {
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();
}
}
2、get()
根据指定下标,到原数组中读取元素。读取过程中不加锁,允许多个线程并发读取。但是如果读取的时候,有其它线程向集合中添加新元素,此时仍然读取到的是旧数据。因为添加操作没有对原数组加锁。
源码展示
public E get(int index) {
// 根据指定下标,从原数组中读取元素
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
3、remove()
删除指定下标元素。根据指定下标,从原数组中,Copy复制其它元素至新数组,最后替换原数组。
源码展示
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)
// 复制原数组中,除最后一个元素以外的所有元素,至新数组
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();
}
}
(4)CopyOnWriteArrayList 的特性
- 在保证并发读取的前提下,确保了写入时的线程安全;
- 由于每次写入操作时,进行了Copy复制原数组,所以无需扩容;
- 适合读多写少的应用场景。由于add()、set() 、 remove()等修改操作需要复制整个数组,所以会有内存开销大的问题。
- CopyOnWriteArrayList 由于只在写入时加锁,所以只能保证数据的最终一致性,不能保证数据的实时一致性
3、ConcurrentHashMap
(1)概述
ConcurrentHashMap是一个支持高并发更新与查询的哈希表(类似HashMap)。在保证安全的前提下,查询不需要锁定。与Hashtable不同,该类不依赖于synchronization去保证线程操作的安全。
(2)数据结构
1、JDK 1.7
分段数组+链表,采用分段锁(Segment)对数组进行分割分段每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率;
过程:
- 首先将数据分为一段一段的存储
- 然后给每一段数据配一把锁
- 当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问
- 一个 ConcurrentHashMap 里包含一个 Segment 数组
- Segment 的结构和 HashMap 类似,是一种数组和链表结构
- 一个 Segment 包含一个 HashEntry 数组
- 每个 Entry 是一个链表结构的元素
- 每个 Segment 守护着一个 Entry 数组里的元素
- 当对 HashEntry 数[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PzEZ2Few-1689591900532)(E:\APESOURCE\学习笔记\image\JDK1.7 ConcurrentHashMap 结构.png)]组的数据进行修改时,必须首先获得对应的 Segment 的锁。
2、JDK1.8
数组+链表 + 红黑树,使用 synchronized 和 CAS 来进行并发控制;
synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率提升 N 倍;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qIqohnnl-1689591900533)(E:\APESOURCE\学习笔记\image\Java 8 ConcurrentHashMap 结构.png)]