Java如何解决ArrayList的并发问题

 更新时间:2025年04月30日 09:44:30   作者:Roc.Chang  
这篇文章主要介绍了Java如何解决ArrayList的并发问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

ArrayListjava.util包中的一个类,它不是线程安全的。

如果多个线程同时对同一个ArrayList进行操作,可能会导致并发问题,如数据不一致ConcurrentModificationException异常。

1. 场景复现

1.1 数据不一致问题示例代码

import java.util.ArrayList;
import java.util.List;

public class ArrayListConcurrencyExample {
    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<>();

        // 创建并启动多个线程,同时向ArrayList添加元素
        Runnable addTask = () -> {
            for (int i = 0; i < 1000; i++) {
                arrayList.add(i);
            }
        };

        Thread thread1 = new Thread(addTask);
        Thread thread2 = new Thread(addTask);

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 输出ArrayList的大小,不一定是预期的 2000 
        System.out.println("Size of arrayList: " + arrayList.size());
    }
}

1.2 ConcurrentModificationException 问题示例代码

ConcurrentModificationException通常会在迭代ArrayList(或其他集合)的同时对其进行结构性修改时抛出。

import java.util.ArrayList;
import java.util.List;
import java.util.Iterator;

public class ConcurrentModificationExample {
    public static void main(String[] args) {
        List<String> arrayList = new ArrayList<>();

        arrayList.add("Item1");
        arrayList.add("Item2");
        arrayList.add("Item3");

        // 获取迭代器
        Iterator<String> iterator = arrayList.iterator();

        // 开始迭代
        while (iterator.hasNext()) {
            String item = iterator.next();
            System.out.println(item);

            // 在迭代过程中尝试修改ArrayList的结构,会引发ConcurrentModificationException
            if (item.equals("Item2")) {
                arrayList.remove(item);
            }
        }
    }
}

当处理ArrayList的并发问题时,不同的方法有不同的细节和适用场景。以下是对每种方法的详细解释:

2. 解决并发的三种方法

2.1 使用 Collections.synchronizedList

使用 Collections.synchronizedList 创建线程安全的ArrayList这是一种简单的方式来使ArrayList线程安全。

它实际上是包装了一个原始的ArrayList,并在每个方法上添加synchronized关键字来确保每个方法在同一时间只能由一个线程访问。

List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());

这种方法适用于那些多数情况下是读操作,但偶尔需要写操作的情况。

请注意,尽管每个方法都是线程安全的,但多个操作之间没有原子性保证,因此还需要其他方式来确保多个操作的一致性。

例如下面的代码就会出现并发问题:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class SynchronizedListExample {
    public static void main(String[] args) {
        List<Integer> synchronizedList = Collections.synchronizedList(new ArrayList<>());

        Runnable addAndRemoveTask = () -> {
            for (int i = 0; i < 1000; i++) {
                synchronizedList.add(i);
                synchronizedList.remove(synchronizedList.size() - 1);
            }
        };

        Thread thread1 = new Thread(addAndRemoveTask);
        Thread thread2 = new Thread(addAndRemoveTask);

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Size of synchronizedList: " + synchronizedList.size());
    }
}

在这个示例中,两个线程同时执行addremove操作。虽然每个操作本身是线程安全的,但它们的组合会导致竞态条件,多次运行后,会出现下面的情况:

最终列表的大小可能不是预期的 2000

由于两个线程同时进行remove操作,可能导致其中一个线程试图删除一个元素,但在另一个线程之前已经删除了,导致IndexOutOfBoundsException异常或其他不一致的结果

Exception in thread "Thread-0" java.lang.IndexOutOfBoundsException: Index 1 out of bounds for length 1
    at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64)
    at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:70)
    at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:248)
    at java.base/java.util.Objects.checkIndex(Objects.java:372)
    at java.base/java.util.ArrayList.remove(ArrayList.java:536)
    at java.base/java.util.Collections$SynchronizedList.remove(Collections.java:2435)
    at com.test.testlist.SynchronizedListExample.lambda$main$0(SynchronizedListExample.java:14)
    at java.base/java.lang.Thread.run(Thread.java:834)
Size of synchronizedList: 1

这突显了Collections.synchronizedList在某些情况下可能无法提供足够的并发保护,因此需要额外的同步措施或选择更适合并发操作的数据结构。

2.2 使用 CopyOnWriteArrayList(推荐使用)

CopyOnWriteArrayList是一种并发集合,它通过在写操作时创建一个新的副本来解决并发问题。

这意味着读操作不会受到写操作的影响,而且不会抛出ConcurrentModificationException异常。

List<String> list = new CopyOnWriteArrayList<>();

这种方法适用于读操作频繁,写操作较少的情况,因为写操作会比较昂贵。但它非常适用于多线程下的读操作,因为它不需要额外的同步。

2.3 使用显式的同步控制

这种方法需要在需要修改ArrayList的地方使用synchronized块或锁来确保线程安全。

这是一种更精细的控制方法,适用于需要更多控制和协同操作的场景。

List<String> list = new ArrayList<>();

// 在需要修改list的地方加锁
synchronized (list) {
    list.add("item");
}

这种方式要求手动管理锁,通过加锁确保在修改ArrayList时进行同步,以防止多个线程同时访问它。

总结

  • 一般在日常编码中,直接使用 CopyOnWriteArrayList 就能满足很多场景;
  • 但是由于每次进行写操作时,都需要复制整个列表,这会导致写操作的性能较低,尤其在列表很大时。因此,CopyOnWriteArrayList 适用于读操作频繁、写操作较少的场景
  • 使用 CopyOnWriteArrayList 时候,应该避免在迭代过程中修改列表;
  • CopyOnWriteArrayList 的迭代器具有弱一致性,在迭代过程中,迭代器可能无法反映出最新的修改,可能会遗漏或重复元素。如果非要强一致性,那就需要全局锁或分布式锁来处理了。
  • 大多数场景中,更多的还是读多写少;
  • 所以一般解决并发的方法,其实就是让并发写的操作,变成串行的;如果非要保证最终的强一致性,那肯定最终还是串行化处理,非常影响性能。
  • 如果是分布式系统的话,那肯定就要使用分布式锁来处理了。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • 在 Spring Boot 中集成 MinIO 对象存储

    在 Spring Boot 中集成 MinIO 对象存储

    MinIO 是一个开源的对象存储服务器,专注于高性能、分布式和兼容S3 API的存储解决方案,本文将介绍如何在 Spring Boot 应用程序中集成 MinIO,以便您可以轻松地将对象存储集成到您的应用中,需要的朋友可以参考下
    2023-09-09
  • Spring Boot 整合 JWT的方法

    Spring Boot 整合 JWT的方法

    这篇文章主要介绍了Spring Boot 整合 JWT的方法,文中实例代码非常详细,帮助大家更好的理解和学习,感兴趣的朋友可以了解下
    2020-07-07
  • Java 离线中文语音文字识别功能的实现代码

    Java 离线中文语音文字识别功能的实现代码

    这篇文章主要介绍了Java 离线中文语音文字识别,本次使用springboot +maven实现,官方demo为springboot+gradle,结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-07-07
  • RabbitMQ中的Connection和Channel信道详解

    RabbitMQ中的Connection和Channel信道详解

    这篇文章主要介绍了RabbitMQ中的Connection和Channel信道详解,信道是建立在 Connection 之上的虚拟连接,RabbitMQ 处理的每条 AMQP 指令都是通过信道完成的,需要的朋友可以参考下
    2023-08-08
  • SpringBoot拦截器实现登录拦截的示例代码

    SpringBoot拦截器实现登录拦截的示例代码

    本文主要介绍了SpringBoot拦截器实现登录拦截,文中根据实例编码详细介绍的十分详尽,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03
  • Struts2实现多文件上传功能

    Struts2实现多文件上传功能

    这篇文章主要为大家详细介绍了Struts2实现多文件上传功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-08-08
  • java中删除文件/文件夹的3种方法示例小结

    java中删除文件/文件夹的3种方法示例小结

    这篇文章主要介绍了java中删除文件/文件夹的3种方法示例小结,第一种是通过io删除文件,第二种是通过Files.walk删除文件,第三种是通过 Files.walkFileTree删除文件,本文结合示例代码给大家介绍的非常详细,需要的朋友参考下吧
    2023-10-10
  • Java 替换空格

    Java 替换空格

    本文主要介绍了Java中替换空格的方法。具有很好的参考价值,下面跟着小编一起来看下吧
    2017-01-01
  • 解决Springboot不能自动提交数据库连接问题

    解决Springboot不能自动提交数据库连接问题

    在使用SSM框架开发时,若在同一Service内部方法间互相调用,直接使用this关键字会导致事务管理失效,从而引发如数据库连接不足等问题,原因是通过this调用不会经过Spring的代理,因此不会自动进行事务处理
    2024-09-09
  • SSH 框架简介

    SSH 框架简介

    SSH是 struts+spring+hibernate的一个集成框架,是目前较流行的一种web应用程序开源框架。本文给大家详细看一下组成SSH的这三个框架
    2017-09-09

最新评论