Java如何解决ArrayList的并发问题
arraylist
是java.util
包中的一个类,它不是线程安全的。
如果多个线程同时对同一个arraylist
进行操作,可能会导致并发问题,如数据不一致或concurrentmodificationexception
异常。
1. 场景复现
1.1 数据不一致问题示例代码
import java.util.arraylist; import java.util.list; public class arraylistconcurrencyexample { public static void main(string[] args) { listarraylist = 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) { listarraylist = new arraylist<>(); arraylist.add("item1"); arraylist.add("item2"); arraylist.add("item3"); // 获取迭代器 iterator 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
关键字来确保每个方法在同一时间只能由一个线程访问。
listsynchronizedlist = 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) { listsynchronizedlist = 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()); } }
在这个示例中,两个线程同时执行add
和remove
操作。虽然每个操作本身是线程安全的,但它们的组合会导致竞态条件,多次运行后,会出现下面的情况:
最终列表的大小可能不是预期的 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
异常。
listlist = new copyonwritearraylist<>();
这种方法适用于读操作频繁,写操作较少的情况,因为写操作会比较昂贵。但它非常适用于多线程下的读操作,因为它不需要额外的同步。
2.3 使用显式的同步控制
这种方法需要在需要修改arraylist
的地方使用synchronized
块或锁来确保线程安全。
这是一种更精细的控制方法,适用于需要更多控制和协同操作的场景。
listlist = new arraylist<>(); // 在需要修改list的地方加锁 synchronized (list) { list.add("item"); }
这种方式要求手动管理锁,通过加锁确保在修改arraylist
时进行同步,以防止多个线程同时访问它。
总结
- 一般在日常编码中,直接使用
copyonwritearraylist
就能满足很多场景; - 但是由于每次进行写操作时,都需要复制整个列表,这会导致写操作的性能较低,尤其在列表很大时。因此,
copyonwritearraylist
适用于读操作频繁、写操作较少的场景。 - 使用
copyonwritearraylist
时候,应该避免在迭代过程中修改列表; copyonwritearraylist
的迭代器具有弱一致性,在迭代过程中,迭代器可能无法反映出最新的修改,可能会遗漏或重复元素。如果非要强一致性,那就需要全局锁或分布式锁来处理了。- 大多数场景中,更多的还是读多写少;
- 所以一般解决并发的方法,其实就是让并发写的操作,变成串行的;如果非要保证最终的强一致性,那肯定最终还是串行化处理,非常影响性能。
- 如果是分布式系统的话,那肯定就要使用分布式锁来处理了。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
推荐阅读
-
IDEA中使用Gradle构建项目中文报GBK错误的解决方案
-
将Java应用做成exe可执行软件的流程步骤
-
SpringBoot实现多种来源的Zip多层目录打包下载
需要将一批文件(可能分布在不同目录、不同来源)打包成zip格式,按目录结构导出给用户下载。1.核心思路支持将本地服务器上的文...
-
Java中减少if-else的设计模式和优化技巧
前言“过于依赖if-else不仅会让代码变得臃肿不堪,还会使维护成本大大增加。其实,if-else虽然是最基础的条件分支,...
-
Spring Boot 中使用 Drools 规则引擎的完整步骤
-
Spring Boot整合Drools规则引擎实战指南及最佳实践
一、drools简介与核心概念1.1什么是drools?drools是redhat旗下的开源业务规则管理系统(brms),...
-
Springboot项目瘦身之如何将jar包与lib依赖分开打包
将jar包与lib依赖分开打包方法一:项目和依赖完全分离maven-jar-plugin负责生成jar文件(jar文件中...
-
Spring动态修改bean属性配置key的几种方法
静态配置的局限性先来看一个典型场景。假设我们有一个数据源配置类:@configuration@configurationpr...
-
Java如何判断一个IP是否在给定的网段内
-
从零开始学java之二叉树和哈希表实现代码