[疑难杂症2024-001] java多线程运行时遇到java.util.ConcurrentModificationException的解决方案

本文主要是介绍[疑难杂症2024-001] java多线程运行时遇到java.util.ConcurrentModificationException的解决方案,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本文由Markdown语法编辑器编辑完成。

1.背景

由于近日在改进一个医学图像的收图服务。之前的版本,我们采用了pynetdicom的服务。
https://pydicom.github.io/pynetdicom/stable/

它的介绍为:
pynetdicom is a pure Python package that implements the DICOM networking protocol. Working with pydicom, it allows the easy creation of DICOM Application Entities (AEs), which can then act as Service Class Users (SCUs) and Service Class Providers (SCPs) by associating with other AEs and using or providing the services available to the association.

是用python实现了DICOM的网络传输协议。这对于快速搭建服务,是很方便的一个选择。但是在实际的应用中,我们发现,如果医院同时有多个终端,比如多台CT机,多个PACS客户端,同时向它发送请求时,它的性能会遇到一定的瓶颈。

因此,我们最终还是采用了基于java的dcm4chee来实现。

具体在实现时,我们会遇到一个处理场景是,需要开启两个线程。
一个线程负责持续不断的接收CT机器发过来的图像;另一个线程,则需要定时不间断地(比如,每隔3s), 进行一次轮询,将符合一定条件的图像,拿走进行后续的处理。

这个场景,非常类似于银行账户。比如同一时刻或非常接近的时刻,有一个账户,有人往这个账户存钱,而有人则从这个账户取钱。那么这两个行为,就相当于是两个运行中的线程。而账户的钱,实际类似于数据库中的记录。一个线程,是要增加账户的钱,另一个账户,则是要减少这个账户的钱。
在这里插入图片描述

因此,如何来保证,这个账户的钱,计算是准确的呢。也即,如何确保两个线程,在修改同一个对象的值时,不至于发生错乱,导致不一致的情况呢?

我们会采用锁的机制。哪个线程先来访问,那么它就先拿到锁,然后进行一系列的操作,操作完成后,再释放锁。当另一个线程也需要访问这个对象时,如果发现被锁了,则会等待,直到这个锁被释放后,它才可以进行操作。

2. 问题根源

虽然我也知道多线程访问时需要上锁。但是在实际编写代码时,还是忽略了一个地方。导致,我加的定时轮询任务,在没有过多数据访问时,还是可以正常工作的。
但是一旦大量的数据过来时,定时任务就罢工了。因此,数据就变成了只进不出,造成了数据处理的堆积。

后来通过在代码的入口处,增加了try…catch的异常捕获逻辑。
再次运行时,可以看到在某一次定时任务运行的时候,发生了异常。异常的名称为ConcurrentModificationException。

顾名思义,这个异常就是说明了,有不同的线程,在同时修改一个变量(在我的项目,这里是一个HashMap)。也就是说,一个线程在往这个HashMap里面,新增元素;而另一个线程,则在轮询判断这个HashMap中是否有符合条件,应该被pop掉的元素。

解决方案:

2.1 使用迭代器

使用Iterator进行遍历和修改:在遍历集合时,使用Iterator的remove()方法来删除元素,而不是直接在集合上进行修改。

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;public class AvoidConcurrentModification {public static void main(String[] args) {List<Integer> numbers = new ArrayList<>();numbers.add(1);numbers.add(2);numbers.add(3);numbers.add(4);# 定义一个迭代器,通过遍历迭代器的方式,可以实现边迭代,边修改的操作。Iterator<Integer> iterator = numbers.iterator();while (iterator.hasNext()) {Integer number = iterator.next();if (number == 2) {iterator.remove(); // 使用迭代器的remove()方法}}}
}

2.2 使用并发集合(Concurrent Collections)

Java提供了一些线程安全的集合类,如ConcurrentHashMap、CopyOnWriteArrayList等。这些集合类使用了特定的并发控制机制,可以在多线程环境下安全地进行遍历和修改操作。根据具体的需求,选择合适的并发集合类来代替普通的集合类。

2.3 使用同步(Synchronization)

如果你必须使用普通的集合类,并且需要在多线程环境下进行遍历和修改操作,你可以使用同步机制来确保线程安全。使用synchronized关键字或者使用Collections.synchronizedXXX()方法包装集合类,以确保每次只有一个线程可以访问集合的修改操作。比如:

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;public class AvoidConcurrentModification {public static void main(String[] args) {List<Integer> numbers = Collections.synchronizedList(new ArrayList<>());numbers.add(1);numbers.add(2);numbers.add(3);numbers.add(4);synchronized (numbers) {Iterator<Integer> iterator = numbers.iterator();while (iterator.hasNext()) {Integer number = iterator.next();if (number == 2) {iterator.remove();}}}}
}

2.4 使用线程锁

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;private Lock lock = new ReentrantLock();import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;public class AvoidConcurrentModification {List<Integer> numbers = Collections.synchronizedList(new ArrayList<>());public static void addItem(String[] args) {# 首先拿到线程锁lock.lock();numbers.add(1);numbers.add(2);numbers.add(3);numbers.add(4);# 处理完毕后,释放线程锁.lock.unlock();}public static void removeItem(String[] args) {# 首先拿到线程锁lock.lock();for (String item : numbers ) {if (item == 2) {numbers.remove(item);}# 处理完毕后,释放线程锁.lock.unlock();}
}

这篇关于[疑难杂症2024-001] java多线程运行时遇到java.util.ConcurrentModificationException的解决方案的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/694361

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

部署Vue项目到服务器后404错误的原因及解决方案

《部署Vue项目到服务器后404错误的原因及解决方案》文章介绍了Vue项目部署步骤以及404错误的解决方案,部署步骤包括构建项目、上传文件、配置Web服务器、重启Nginx和访问域名,404错误通常是... 目录一、vue项目部署步骤二、404错误原因及解决方案错误场景原因分析解决方案一、Vue项目部署步骤

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.