五.Java 中线程安全的集合类
线程安全的集合类 , 其核心解决普通集合(如 ArrayList/HashMap)在并发读写时的数据错乱 , ConcurrentMidificationException , 此循环等问题
1.线程安全集合的核心分类
|--------------|---------------------------------|------------------------------------------------------------------|-------------------------------|
| 分类 | 实现原理 | 代表类 | 核心特点 |
| 同步包装类 | Collections.synchronizedXxx() | synchronizedList/synchronizedMap | 全局加锁(synchronized),简单但并发性能低 |
| JUC 并发集合 | 分段锁 / CAS / 写时复制 / 阻塞队列 | ConcurrentHashMap/CopyOnWriteArrayList/LinkedBlockingQueue | 精细化锁 / 无锁设计,高并发下性能更优 |
2.多线程下使用 ArrayList
① 自己使用同步机制(synchronized 或 ReentrantLock)
此处不再说
② 通过 Collection 工具类为普通集合套上 [ 全局锁 ]
本质是对集合所有方法加 synchronized
java
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
注意 : 迭代时需要手动加锁 , 否则抛异常(ConcurrentMidificationException)
java
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class demo1 {
public static void main(String[] args) throws InterruptedException {
//多线程下的ArraryList
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
syncList.add("Hello");
syncList.add("World");
new Thread(new Runnable() {
@Override
public void run() {
syncList.add("Java");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
syncList.add("Thread");
}
}).start();
Thread.sleep(1000);
synchronized (syncList) {//必须锁定集合对象本身
for (String s : syncList) {
System.out.println(s);
}
}
}
}
③ 使用 JUC 包 , CopyOnWriteArrayList
核心实现 :
- 写操作 : (add/remove/set) 复制一份新的数组 , 在数组上修改 , 修改完成后替换原数组 , 全程加锁
- 读操作 : (get/iterator) 直接读原数组 , 无锁 , 性能极高
常见问题 :
问题 1 : 写操作性能地 , 内存开销大
解决方案 : 仅用于都铎写少的场景 ; 当频繁场景时改用 ReentrantLock 手动加锁的普通 List
问题 2 : 迭代过程中其他线程的修改不会反映到迭代器中
解决方案 : 需要加锁 ; 或者每次迭代前重新获取集合
3.多线程使用队列
① ArrayBlockingQueue
基于数组实现的阻塞队列
② LinkedBlockingQueue
基于链表实现的阻塞队列
③ PriorityBlockingQueue
基于堆实现的带优先级的阻塞队列
④ TransferQueue
最多只包含一个元素的阻塞队列
4.多线程使用哈希表
在多线程下使用哈希表(键值对储存) , 核心问题是解决线程安全和并发性能--普通 HashMap 线程不安全 , 并发读写会导致数据错乱 , 死循环 , ConcurrentModificationException 等问题
① 线程安全哈希表
|---------|--------------------------------------------|------------------------------------------|
| 类型 | 代表类 | 实现原理 |
| 全局锁哈希表 | Hashtable/ Collections.synchronizedMap | 所有方法加 synchronized;全局锁 |
| 精细化锁哈希表 | ConcurrentHashMap | JDK1.7:分段锁;JDK1.8:CAS + 局部synchronized |
| 写时复制哈希表 | ConcurrentSkipListMap(有序) | 跳表 + CAS,无锁设计 |
②Hashtable(低效)
只是简单的把关键的方法加上了 synchronized
这相当于直接对 Hashtable 对象本身加锁
- 如果多线程访问同一个 Hashtable 就会直接造成锁冲突
- 一旦触发扩容 , 就由该线程完成整个扩容过程
注意 : Hashtable 是 JDK1.0 的老旧类 , 性能差 , 已被ConcurrentHashMap 完全替代 , 仅作为了解
②ConcurrentHashMap(首选)
核心原理: 抛弃了 JDK1.7 的分段锁(把这些链表分成几组 , 每个组安排一个锁) , 改用更细粒度的锁机制 (锁桶)
- 读操作 : 无锁 , 通过 volatile 保证数据可见性
- 写操作 : 1)对空桶 : CAS 原子操作写入 , 无锁 ; 2) 对非空桶 : 对桶首届点加 synchronized 锁 , 仅阻塞当前桶的读写 , 其他桶可并发
- 当桶元素超过 8 个转为红黑树 , 提升查找性能
java
private static final Map<String, Integer> map = new ConcurrentHashMap<>();
扩容机制 : 化整为零
- 发现扩容的线程只创建新数组,搬几个元素 : 1) 触发扩容的线程先标记 , 并且创建新数组(容量翻倍) ; 2) 按步长 (16)拆分原数组 , 该线程仅迁移自己负责的一小段桶(目的:避免的那现场一次迁移所有元素导致长时间阻塞)
- 扩容期间新老数组同时存在 : 1)新数组作为全局变量 , 扩容全程与老数组共存; 2)迁移完成的桶做标记 , 未迁移的桶仍在老数组中(保证连续性)
- 后续线程搬家 , 各搬一小部分 : 1)任何线程执行 put/remove 时 , 若检测到扩容中 , 会先暂停自身操作 , 协助迁移 ; 2)每个线程仅迁移自己"认领的桶段"(避免重复)
- 搬完最后一个元素删除老数组 : 1)所有桶迁移完成 , 主线程将老数组替换成新数组 , 并删除 (释放内存); 2)更新扩容阈值 , 并标记扩容完成
- 插入只往新数组加 : 扩容期间 , put 操作先检查桶是否已经迁移 , 若已迁移 : 直接写入新数组 , 若未迁移 : 先协助迁移 , 再写入新数组(防止老数组数据冗余)
- 查找需同时查找新老数组 : get 操作 , 先查老数组 , 若桶已迁移完(标记) , 则跳转到行数则查询 ; 若桶未迁移 : 直接查询老数组 ; 若新老数组都查不到 , 再返回 null(保证扩容期间读取数据不丢失 , 不遗漏)