java中在多线程的情况下安全的修改list

在Java中,ArrayListLinkedList等常见List实现类不是线程安全的 (非同步)。当多个线程同时对其进行修改(如addremove)或读写操作时,可能会导致数据不一致、ConcurrentModificationException(并发修改异常)等问题。

要在多线程环境下安全地修改List,需通过线程安全的容器同步机制保证操作的原子性和可见性。以下是常用解决方案及实现方式:

一、使用线程安全的List实现类

Java提供了几种线程安全的List实现,可直接替换非线程安全的List,无需手动处理同步。

1. Vector(古老实现,不推荐)

Vector是Java早期的线程安全List实现,其所有方法都被synchronized修饰(同步方法),保证线程安全。
缺点 :同步粒度太粗(整个方法加锁),多线程并发效率低,且功能上被更优的方案替代,不推荐在新代码中使用

java 复制代码
// Vector是线程安全的,但性能较差
List<String> vector = new Vector<>();
// 多线程可安全调用add/remove等方法
vector.add("A");
vector.remove(0);
2. Collections.synchronizedList()(包装同步,推荐基础场景)

Collections工具类的synchronizedList()方法可将任意非线程安全的List包装为线程安全的List 。其原理是对所有方法添加同步锁(使用synchronized块),保证同一时刻只有一个线程能操作List

使用方式

java 复制代码
// 1. 创建非线程安全的List(如ArrayList)
List<String> unsafeList = new ArrayList<>();
// 2. 包装为线程安全的List
List<String> safeList = Collections.synchronizedList(unsafeList);

// 多线程环境下可安全操作
// 线程1:添加元素
new Thread(() -> {
    safeList.add("A");
}).start();

// 线程2:删除元素
new Thread(() -> {
    if (!safeList.isEmpty()) {
        safeList.remove(0);
    }
}).start();

注意事项

  • 迭代操作需手动加锁:synchronizedList返回的List在迭代时(如for-eachiterator不自动同步 ,需手动用synchronized块包裹,否则可能抛出ConcurrentModificationException

    java 复制代码
    // 迭代时必须手动同步(锁对象为safeList本身)
    synchronized (safeList) {
        for (String s : safeList) {
            System.out.println(s);
        }
    }
  • 适合读写频率均衡的场景:由于所有操作都加锁,高并发下性能一般,但实现简单,适合大多数基础场景。

3. CopyOnWriteArrayList(写时复制,推荐读多写少场景)

CopyOnWriteArrayList是Java并发包(java.util.concurrent)提供的线程安全List,其核心原理是**"写时复制"**:

  • 读操作:无需加锁,直接访问当前数组(性能极高)。
  • 写操作(addremove等):先复制一份新的数组,在新数组上修改,然后将引用指向新数组(修改时加锁,保证原子性)。

适用场景:读操作远多于写操作(如缓存、配置列表),写操作频率低但读操作需高效。

使用方式

java 复制代码
import java.util.concurrent.CopyOnWriteArrayList;

// 初始化线程安全的CopyOnWriteArrayList
List<String> cowList = new CopyOnWriteArrayList<>();

// 多线程安全操作
// 线程1:添加元素(写操作,会复制数组)
new Thread(() -> {
    cowList.add("A");
}).start();

// 线程2:读取元素(读操作,无锁,直接访问)
new Thread(() -> {
    for (String s : cowList) {
        System.out.println(s);
    }
}).start();

优点

  • 读操作无锁,并发性能极佳(适合读多写少)。
  • 迭代时不会抛出ConcurrentModificationException(迭代的是旧数组的快照)。

缺点

  • 写操作成本高(复制数组,内存占用翻倍)。
  • 数据实时性差(读操作可能访问的是旧数组,修改后的数据需等新数组替换后才能被读取)。

二、手动同步(锁机制)

如果需要更灵活地控制同步粒度(如仅对关键修改操作加锁),可使用synchronized关键字或Lock接口手动实现同步。

1. 使用synchronized

通过synchronized锁定List对象或其他锁对象,保证同一时刻只有一个线程执行修改操作。

java 复制代码
List<String> list = new ArrayList<>();
// 定义锁对象(也可直接用list本身作为锁)
Object lock = new Object();

// 线程1:添加元素
new Thread(() -> {
    synchronized (lock) { // 加锁
        list.add("A");
    }
}).start();

// 线程2:删除元素
new Thread(() -> {
    synchronized (lock) { // 加锁
        if (!list.isEmpty()) {
            list.remove(0);
        }
    }
}).start();
2. 使用ReentrantLock(可重入锁)

java.util.concurrent.locks.ReentrantLock提供比synchronized更灵活的锁控制(如超时锁、公平锁),适合复杂场景。

java 复制代码
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

List<String> list = new ArrayList<>();
// 创建锁对象(可指定为公平锁,按请求顺序获取锁)
Lock lock = new ReentrantLock(true);

// 线程1:添加元素
new Thread(() -> {
    lock.lock(); // 加锁
    try {
        list.add("A");
    } finally {
        lock.unlock(); // 必须在finally中释放锁,避免死锁
    }
}).start();

// 线程2:删除元素
new Thread(() -> {
    lock.lock(); // 加锁
    try {
        if (!list.isEmpty()) {
            list.remove(0);
        }
    } finally {
        lock.unlock();
    }
}).start();

三、注意事项

  1. 复合操作的原子性

    即使使用线程安全的List复合操作(如"先判断再修改")仍需额外同步。例如:

    java 复制代码
    // 错误示例:contains和add是两个独立操作,可能被其他线程打断
    if (!safeList.contains("A")) { 
        safeList.add("A"); // 可能重复添加
    }
    
    // 正确:用同步块保证复合操作原子性
    synchronized (safeList) {
        if (!safeList.contains("A")) {
            safeList.add("A");
        }
    }
  2. 迭代器的线程安全

    • synchronizedList的迭代器需手动同步(见上文)。
    • CopyOnWriteArrayList的迭代器是"快照迭代器",不支持removeadd等修改操作(会抛UnsupportedOperationException),只能遍历。
  3. 性能权衡

    • 读多写少:优先CopyOnWriteArrayList(读无锁)。
    • 读写均衡或写操作频繁:优先Collections.synchronizedList()或手动锁(避免CopyOnWriteArrayList的复制开销)。
    • 避免使用Vector(性能差,已过时)。

总结

多线程安全修改List的核心是保证操作的原子性和可见性,常用方案对比:

方案 原理 优点 缺点 适用场景
Vector 同步方法 简单直接 性能差,同步粒度粗 兼容旧代码(不推荐新用)
synchronizedList 同步块包装 适配所有List,实现简单 所有操作加锁,并发性能一般 读写均衡的基础场景
CopyOnWriteArrayList 写时复制 读操作无锁,性能极佳 写操作成本高,数据实时性差 读多写少(如缓存、配置)
手动锁(synchronized/Lock 自定义同步粒度 灵活控制锁范围 需手动处理锁释放,易出错 复杂场景(如复合操作)

根据实际业务的读写频率和复杂度选择合适方案即可。

相关推荐
我会冲击波1 分钟前
Easy Naming for IDEA:从命名到注释,您的编码效率助推器
java·intellij idea
池以遇2 分钟前
云原生高级---TOMCAT
java·tomcat
蚰蜒螟19 分钟前
JVM安全点轮询汇编函数解析
汇编·jvm·安全
IT毕设实战小研31 分钟前
Java毕业设计选题推荐 |基于SpringBoot的水产养殖管理系统 智能水产养殖监测系统 水产养殖小程序
java·开发语言·vue.js·spring boot·毕业设计·课程设计
小小深36 分钟前
Spring进阶(八股篇)
java·spring boot·spring
京东云开发者1 小时前
虚引用GC耗时分析优化(由 1.2 降低至 0.1 秒)
java
Java中文社群1 小时前
求职必备!常用拖Offer话术总结
java·后端·面试
Techie峰1 小时前
Redis Key过期事件监听Java实现
java·数据库·redis
JosieBook1 小时前
【SpringBoot】12 核心功能-配置文件详解:Properties与YAML配置文件
java·spring boot·后端
开发者如是说1 小时前
[中英双语] 如何防止你的 Android 应用被破解
android·安全