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();
        }
    }
相关推荐
谢亮_vipxieliang1 小时前
Spring 事务失效的常见场景
java·开发语言·数据库·spring boot
郑州光合科技余经理1 小时前
海外版外卖加盟:总站与分站配送规则怎么分开管
java·开发语言·前端·后端·uni-app·php·ai编程
2601_962071571 小时前
类变量和全局变量的生命周期有什么区别?
java·开发语言·jvm
code2cat2 小时前
Java进阶篇之AtomicReference:一次替换引用,读到一份完整状态
java·并发编程·不可变对象·atomicreference
专业程序开发源2 小时前
springboot简历管理系统81389-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·php·课程设计
Wang's Blog2 小时前
Java 项目实战: 外卖平台优化-前后端分离开发模式与工程拆分
java·开发语言
专业程序开发源2 小时前
springboot社区养老系统44071-计算机课程设计、毕业设计
vue.js·spring boot·后端·python·django·php·课程设计
h_a_o777oah3 小时前
【树论】树链剖分:将树形结构转化为线性结构以处理树上路径问题(洛谷 P3384 附C++代码模板)
数据结构·算法·线段树·acm·树链剖分·树论·重链剖分
余槐i3 小时前
数据没回滚也不报错:@Transactional 自调用失效的 3 种复现与修复
spring boot·mysql·多线程·spring 事务
HEJOO93 小时前
指针的算术运算详解
java·数据结构·算法