文章提示
-
🔍 重点掌握:线程不安全现象的表现形式及底层原理
-
⚠️ 避坑指南:高并发场景下直接使用非线程安全集合的风险
-
🛠️ 实战技巧:通过代码案例快速验证不同解决方案的效果
-
💡 拓展思维:结合JUC包理解Java并发设计的哲学
目录
方案2:Collections.synchronizedList
方案3:CopyOnWriteArrayList(写时复制)
前言
在多线程编程中,集合的线程安全问题是引发生产事故的"隐形炸弹"。本文将以ArrayList
、HashSet
、HashMap
三大常用集合为切入点,通过代码实例演示线程不安全现象,深度解析Vector
、Collections.synchronizedList
、CopyOnWriteArrayList
等解决方案的实现原理。无论你是刚接触并发编程的新手,还是需要巩固知识的老手,本文都将为你提供清晰易懂的实践指南。
一、ArrayList的线程安全问题与解决方案
1.1 问题现象演示
package JUC.Usafe;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class ArrayListDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// 创建10个线程并发添加元素
for (int i = 0; i < 10; i++) {
new Thread(() -> {
//向集合还行添加内容
list.add(UUID.randomUUID().toString().substring(0, 8));
//打印集合内容
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
运行结果分析:
问题根源:
-
数据覆盖:多个线程同时执行
add()
导致elementData数组越界 -
扩容竞争:当需要扩容时,size的可见性问题导致数组越界
java.util.ConcurrentModificationException 是由于在多线程环境下,一个线程正在对集合进行迭代(如 System.out.println(list) 的内部实现会调用 toString() 方法),而另一个线程同时修改了集合的内容(如 list.add() 操作)。这种情况下,ArrayList 的迭代器检测到集合被修改后抛出了此异常。
ArrayList 并不是线程安全的集合类。在多线程环境中,如果需要对集合进行并发操作,必须采取适当的同步机制或使用线程安全的集合类。
1.2 解决方案对比
方案1:Vector(同步方法)
List<String> list = new Vector<>();
实现原理:
-
所有方法都添加
synchronized
关键字 -
默认扩容2倍(ArrayList是1.5倍)
优缺点:
-
✅ 保证强一致性
-
❌ 锁粒度大,性能差(吞吐量约下降60%)
方案2:Collections.synchronizedList
List<String> list = Collections.synchronizedList(new ArrayList<>());
import java.util.*;
import java.util.concurrent.*;
public class ArrayListDemo {
public static void main(String[] args) {
// 使用 synchronizedList 包装 ArrayList
List<String> list = Collections.synchronizedList(new ArrayList<>());
// 创建10个线程并发添加元素
for (int i = 0; i < 10; i++) {
new Thread(() -> {
// 向集合中添加内容
list.add(UUID.randomUUID().toString().substring(0, 8));
// 打印集合内容时需要显式加锁
synchronized (list) {
System.out.println(list);
}
}, String.valueOf(i)).start();
}
}
}
实现特点:
-
使用同步代码块包裹所有操作
-
通过
mutex
对象作为锁
源码:
public boolean add(E e) {
synchronized (mutex) {
return c.add(e);
}
}
适用场景:
-
需要兼容老代码的改造
-
对性能要求不高的场景
方案3:CopyOnWriteArrayList(写时复制)
写时复制是指:在并发访问的情景下,当需要修改JAVA中Containers的元素时,不直接修改该容器,而是先复制一份副本,在副本上进行修改。修改完成之后,将指向原来容器的引用指向新的容器(副本容器)。
List<String> list = new CopyOnWriteArrayList<>();
import java.util.*;
import java.util.concurrent.*;
public class ArrayListDemo {
public static void main(String[] args) {
// 使用 CopyOnWriteArrayList 替代 ArrayList
List<String> list = new CopyOnWriteArrayList<>();
// 创建10个线程并发添加元素
for (int i = 0; i < 10; i++) {
new Thread(() -> {
// 向集合中添加内容
list.add(UUID.randomUUID().toString().substring(0, 8));
// 打印集合内容
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
核心原理:
-
写操作时复制新数组(
ReentrantLock
保证单线程写
①由于不会修改原始容器,只修改副本容器(JMM原理)。因此,可以对原始容器进行并发地读。其次,实现了读操作与写操作的分离,读操作发生在原始容器上,写操作发生在副本容器上。
②数据一致性问题:读操作的线程可能不会立即读取到新修改的数据,因为修改操作发生在副本上。但最终修改操作会完成并更新容器,因此这是最终一致性。
2.修改完成后将引用指向新数组
源码:
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();
}
}
性能优势:
-
读操作完全无锁(适合读多写少场景)
-
写操作通过复制避免数据竞争
应用场景:
CopyOnWrite容器适用于读多写少的场景。因为写操作时,需要复制一个容器,造成内存开销很大,也需要根据实际应用把握初始容器的大小。
不适合于数据的强一致性场合。若要求数据修改之后立即能被读到,则不能用写时复制技术。因为它是最终一致性。
4,为什么会出现COW?
集合类(ArrayList、HashMap)上的常用操作是:向集合中添加元素、删除元素、遍历集合中的元素然后进行某种操作。当多个线程并发地对一个集合对象执行这些操作时就会引发ConcurrentModificationException,比如线程A在for-each中遍历ArrayList,而线程B同时又在删除ArrayList中的元素,就可能会抛出ConcurrentModificationException,可以在线程A遍历ArrayList时加锁,但由于遍历操作是一种常见的操作,加锁之后会影响程序的性能,因此for-each遍历选择了不对ArrayList加锁而是当有多个线程修改ArrayList时抛出ConcurrentModificationException,因此,这是一种设计上的权衡。
为了应对多线程并发修改这种情况,一种策略就是本文的主题“写时复制”机制;另一种策略是:线程安全的容器类:
ArrayList—>CopyOnWriteArrayList
HashSet—>CopyOnWriteHashSet
HashMap—>ConcurrentHashMap
而ConcurrentHashMap并不是从“复制”这个角度来应对多线程并发修改,而是引入了分段锁(JDK7);CAS解决多线程并发修改的问题。
package JUC.Usafe;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteList {
public static void main(String[] args) {
// 并发下 ArrayList 不安全的吗,Synchronized;
/**
* 解决方案;
* 1、List<String> list = new Vector<>();
* 2、List<String> list = Collections.synchronizedList(new ArrayList<>
());
* 3、List<String> list = new CopyOnWriteArrayList<>();
*/
// CopyOnWrite 写入时复制 COW 计算机程序设计领域的一种优化策略;
// 多个线程调用的时候,list,读取的时候,固定的,写入(覆盖)
// 在写入的时候避免覆盖,造成数据问题!
// 读写分离
// CopyOnWriteArrayList 比 Vector Nb 在哪里?
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
二、HashSet线程安全问题与解决方案
2.1 问题复现
Set<String> set = new HashSet<>();
// 多线程并发add操作(代码类似ArrayList示例)
底层原理:
-
HashSet实际使用HashMap存储元素
public boolean add(E e) {
return map.put(e, PRESENT)==null; // PRESENT是固定Object对象
}
2.2 解决方案
Set<String> set = new CopyOnWriteArraySet<>();
实现特点:
-
基于
CopyOnWriteArrayList
实现 -
通过
addIfAbsent()
保证元素唯一性
package JUC.Usafe;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* 同理可证 : ConcurrentModificationException
* //1、Set<String> set = Collections.synchronizedSet(new HashSet<>());
* //2、CopyOnWriteArraySet
*/
public class CopyOnWriteSet {
public static void main(String[] args) {
// Set<String> set = new HashSet<>();
// Set<String> set = Collections.synchronizedSet(new HashSet<>());
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 1; i <=30 ; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
三、HashMap线程安全问题与解决方案
3.1 并发问题表现
-
JDK1.7及之前:环形链表导致CPU 100%
-
JDK1.8:数据覆盖或size计算错误
3.2 解决方案
Map:
Map<String, Object> map = new ConcurrentHashMap<>();
package JUC.Usafe;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class MapTest {
public static void main(String[] args) {
// map 是这样用的吗? 不是,工作中不用 HashMap
// 默认等价于什么? new HashMap<>(16,0.75);
// Map<String, String> map = new HashMap<>();
// 唯一的一个家庭作业:研究ConcurrentHashMap的原理
Map<String, String> map = new ConcurrentHashMap<>();
for (int i = 1; i <=30; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(
0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
JDK1.7实现:
-
分段锁(Segment继承ReentrantLock)
-
默认16个段,支持16个并发写
JDK1.8优化:
-
改用
Node + synchronized + CAS
-
链表转红黑树优化查询效率
四、解决方案对比总结
方案 | 锁粒度 | 适用场景 | 吞吐量 | 数据一致性 |
---|---|---|---|---|
Vector | 方法级同步 | 兼容老系统 | 低 | 强一致 |
synchronizedList | 代码块同步 | 简单并发场景 | 中 | 强一致 |
CopyOnWriteArrayList | 写时复制 | 读多写少(配置信息等) | 高 | 最终一致 |
ConcurrentHashMap | 分段锁/CAS | 高频并发写入(缓存系统) | 极高 | 弱一致 |
文章总结与升华
📚 核心要点回顾
-
问题本质:集合线程不安全的核心在于共享数据的可见性与原子性
-
示例:两个线程同时执行
list.add()
导致size计数错误
-
-
解决方案选择标准:
-
根据读写比例选择(如CopyOnWrite适合读多写少)
-
根据数据一致性要求选择(强一致 vs 最终一致)
-
-
JUC设计哲学:
-
通过空间换时间(CopyOnWrite)
-
减小锁粒度提升并发度(ConcurrentHashMap分段锁)
-
🚀 技术延伸
-
写时复制(CopyOnWrite)的衍生应用
-
Docker镜像分层存储
-
Redis持久化中的COW机制
// 自定义简易COW实现 class SimpleCOWList<T> { private volatile Object[] array = new Object[0]; public synchronized void add(T item) { Object[] newArray = Arrays.copyOf(array, array.length + 1); newArray[array.length] = item; array = newArray; } public T get(int index) { return (T) array[index]; // 无锁读 } }
-
-
ConcurrentHashMap的进阶用法
-
使用
computeIfAbsent
实现缓存
ConcurrentHashMap<String, Connection> connectionPool = new ConcurrentHashMap<>(); public Connection getConnection(String key) { return connectionPool.computeIfAbsent(key, k -> createConnection(k)); }
-
-
锁的演进趋势:
-
从
synchronized
到ReentrantLock
-
从分段锁到CAS+
synchronized
优化 -
未来趋势:无锁编程(如LongAdder)
-
💡 学习建议
-
实践验证:使用
javap -c
反编译查看synchronized
的实现 -
性能测试:使用JMH对比不同方案的吞吐量差异
-
源码学习:重点研究
CopyOnWriteArrayList
的迭代器实现
读者不仅能够理解集合线程安全的核心原理,还能根据实际业务场景选择合适的并发容器,为构建高并发系统打下坚实基础
参考: