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();
        }
    }
相关推荐
兔小盈2 分钟前
多线程-(五)线程安全之内存可见性
java·开发语言·多线程
CeshirenTester29 分钟前
LangChain的工具调用 vs 原生Skill API:性能差在哪儿?
java·人工智能·langchain
yaoxin52112336 分钟前
400. Java 文件操作基础 - 使用 Buffered Stream I/O 读取文本文件
java·开发语言·python
Fox爱分享38 分钟前
字节二面:10亿数据毫秒级查手机尾号后4位,答不出“异构索引”直接挂?
java·后端·面试
61900833643 分钟前
win idea 控制台中文乱码
java·ide·intellij-idea
折哥的程序人生 · 物流技术专研44 分钟前
《Java面试85题图解版(二)》进阶深化上篇:并发编程 + JVM
java·开发语言·后端·面试
abcnull1 小时前
用ASM做精准测试(Java)
java·jar·asm·字节码·精准测试
@杰克成1 小时前
Java学习26
java·学习·idea
qeen871 小时前
【数据结构】二叉树相关经典函数C语言实现
c语言·数据结构·c++·笔记·学习·算法·二叉树
良木生香2 小时前
【C++初阶】STL——List从入门到应用完全指南(1)
开发语言·数据结构·c++·程序人生·算法·蓝桥杯·学习方法