List线程不安全解决办法和适用场景
ArrayList 多线程下的线程不安全问题
java
import java.util.ArrayList;
import java.util.List;
public class ListThreadUnsafeDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// 10个线程同时向List添加元素
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 100; j++) {
list.add(Thread.currentThread().getName() + "-" + j);
}
}).start();
}
// 等待所有线程执行完成(简单休眠,实际开发用CountDownLatch)
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 预期size=1000,实际大概率小于1000(数据丢失),或直接抛异常
System.out.println("List最终长度:" + list.size());
}
}
问题原因 :ArrayList底层是数组,写操作(add等)无锁保护,多线程同时修改数组大小、元素索引时,会出现操作覆盖 、索引错位,最终导致数据丢失或异常。
List 线程不安全的 2 种核心解决方案
使用CopyOnWriteArrayList(读多写少场景)
读多写少场景的最优解 ,核心原理是写时复制(Copy On Write):
- 读操作:无锁,直接访问底层数组,多线程同时读不阻塞、高性能;
- 写操作(
add/remove/set):先加锁,再复制一份全新的底层数组,在新数组上执行修改,修改完成后将底层数组引用指向新数组,最后释放锁; - 遍历操作:基于原数组遍历,即使遍历中其他线程修改 List,也不会抛
ConcurrentModificationException。
java
// 仅替换为CopyOnWriteArrayList,其余代码不变
List<String> list = new CopyOnWriteArrayList<>();
手动加锁保护普通 ArrayList(灵活可控,写多读少场景推荐)
核心是将多线程的非原子操作包裹在锁范围内,保证操作的原子性。
java
for (int j = 0; j < 100; j++) {
// 核心:将写操作包裹在synchronized代码块中,锁对象为list本身
synchronized (list) {
list.add(Thread.currentThread().getName() + "-" + j);
}
}
使用ReentrantLock可重入锁(灵活,支持公平锁 / 非公平锁,推荐复杂场景)
比synchronized更灵活,支持手动加锁 / 释放锁 、公平锁 (按线程等待顺序获取锁)、尝试获取锁 (tryLock()),适合需要精细控制锁的场景:
java
private static final List<String> list = new ArrayList<>();
// 定义可重入锁(默认非公平锁,传true为公平锁)
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 100; j++) {
lock.lock(); // 加锁
try {
// 核心操作:在锁保护下执行写操作
list.add(Thread.currentThread().getName() + "-" + j);
} finally {
lock.unlock(); // 释放锁(必须在finally中,防止异常导致锁泄漏)
}
}
}).start();
}
拓展:复合操作的线程安全
复合操作指「多个 List 操作组合成的逻辑」(如判断是否为空 → 删除元素、判断是否存在 → 修改元素),这类操作即使使用线程安全 List,也需要额外加锁,否则会出现线程安全问题。
java
private static final List<String> list = new CopyOnWriteArrayList<>();
public static void removeFirst() {
// 复合操作:isEmpty() + remove(0),无锁保护,线程不安全
if (!list.isEmpty()) {
list.remove(0);
}
}
public static void main(String[] args) {
// 先添加元素
list.add("A");
list.add("B");
// 2个线程同时执行removeFirst()
new Thread(ListComplexOperationUnsafe::removeFirst).start();
new Thread(ListComplexOperationUnsafe::removeFirst).start();
}
问题 :两个线程可能同时通过isEmpty()判断,然后同时执行remove(0),导致索引越界异常。
java
public static void removeFirst() {
lock.lock();
try {
// 复合操作包裹在锁中,保证原子性
if (!list.isEmpty()) {
list.remove(0);
}
} finally {
lock.unlock();
}
}