List线程不安全解决办法和适用场景

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();
        }
    }
相关推荐
nanxun88615 小时前
记一次诡异的 Docker 容器"串包"故障排查
java
用户15630681035117 小时前
Day01 | Java 基础(Java SE)
java
行者全栈架构师19 小时前
Maven dependency:tree 的 8 个高级用法
java·后端
行者全栈架构师1 天前
IDEA 中 Maven 项目的 15 个红色报错快速解决方法
java·后端
令人头秃的代码0_01 天前
mac(m5)平台编译openjdk
java
唐青枫2 天前
Java JDBC 实战指南:从 Connection 到事务和连接池
java
一个做软件开发的牛马2 天前
MyBatis-Plus 从零实战:完整搭建可运行 Demo,BaseMapper 零 SQL、Wrapper 条件构造、分页插件与代码生成器详解
java·后端
用户3721574261352 天前
Java 处理 PDF 图片:提取 PDF 中的图片,并压缩 PDF 图片体积
java
用户3721574261352 天前
Java 打印 Word 文档:从基础打印到高级设置
java
用户3521802454753 天前
当 Prompt 学会"热更新":Spring Boot × Nacos3 AI 实战
java·spring boot·ai编程