首页 > 其他分享 >记录一次并发问题的解决

记录一次并发问题的解决

时间:2022-12-19 14:55:45浏览次数:53  
标签:java 记录 list 并发 报错 context 解决 null

并发问题的产生背景

该问题是在生产运行的过程中出现的。这个运行的项目是一个拉取第三方数据的一个服务,该服务会在拉取到数据之后直接将该数据直接插入到本地库,其中插入本地库的操作是调用的一个静态方法,静态方法对数据进行了多次数据处理,并且静态方法是异步执行的。当多个调用一起出现的时候,就相当于启动了多个线程去执行静态方法导致的并发。最终导致数据入库失败,并且程序抛错,具体错误信息如下:

1、错误一
Caused by: java.util.ConcurrentModificationException
	at java.util.LinkedList$ListItr.checkForComodification(LinkedList.java:761)
	at java.util.LinkedList$ListItr.next(LinkedList.java:696)
	at java.util.SubList$1.next(AbstractList.java:696)
2、错误二
java.lang.Index0utOfBoundsException: toIndex = 5
    at java.util.Sublist.<init>(Abstractlist.java:622) -[7:1.8.0 3337
    at java.util.sublist.sublist(abstractlist.java:750) -[?:1.8.0 333]

排查过程

上面的错误信息是截取了真实错误信息中的关键提示,隐藏了部分对代码的提示。我们根据错误信息找到了对应的代码。

private void processor() {
    beforeMethod();
    Context context = connector.request();
    afterEbancCache(context);
}

private void afterEbancCache(Context context) {
    ContextProcesserInsert.insertList(context)
}

public class ContextProcesserInsert {

    public static void insertList(Context context) {
        new Thread(() -> {
            insert(context);
        });
    }

    private void insert(Context context) {
        List<String> list = context.getList();
        // ... 省略几十行,主要计算开始和结束值,是用来分批插入数据库的
        for (xxx) {
            sqlMaperClient.insert(list.subList(startNum, endNum));
        }
    }

}

报错指向:sqlMaperClient.insert(list.subList(startNum, endNum));这一行代码。刚开始的是时候,观察下来这样的代码其实不会直接报错,并且报并发错误,最多报越界异常吧。不过越界异常确实在日志中出现了,所以可以理解,只是ConcurrentModificationException错误就相对来说不是那么容易理解。

ConcurrentModificationException错误的解释

其实看到这错,就已经很明确是list并发操作引起的了。不过这其实不是疑惑的点。不过还是先来看看这个错误的具体解释吧
异常出现的原因具体代码如下:
在这里插入图片描述
上面是我们错误具体报出这个错误的地方,我们可以看到,这个错误就是做了一件事情:在list进行下一个数据的操作之前会调用checkForComodification()方法,然后根据cursor的值获取到元素,接着将cursor的值赋给lastRet,并对cursor的值进行加1操作。初始时,cursor为0,lastRet为-1,那么调用一次之后,cursor的值为1,lastRet的值为0。注意此时,modCount为0,expectedModCount也为0..... 其实总结起来就一句话,就是我操作下一个元素之前,要去检查当前的list的长度是否有过变更,我记录的位置是否有出现错误。
单线程执行的时候,私有了对应的对象,不会出现list长度被更改的情况,但是并发执行,可能就将操作下一个数据变成了操作下下个数据,从而导致,整个list的最终记录出现问题。所以需要检查这个问题,然后发现就抛出错误。

看到这样的解释,其实和上面的表现已经很吻合了。疑惑的地方只有一个,上面案例来说并发不会并发到insert这个方法来,因为insert是实例方法。而且整个代码下来,并没有真正去修改list数据或者list长度的地方。

还原失败

为了能够找到足够的证据,证明上面的错误是并发出现的,我们做了很多的本地测试。该测试主要是为了能够更有效的保证处理并发问题。
期间我们做了一下几件事来验证

  • 使用编写代码的形式,启动1000个线程来跑程序
  • jmeter - 1000线程并发测试
  • 断点线程干预

以上操作均没有得到想要的效果,最终还原失败告终

解决方案

根据上面的推测,我们没有得到最有力的证据,不过大致是看明白了怎么去解决。其中真正引起这个并发的原因应该就是最开始的静态方法引起的,该静态方法经过多层级调用,对list操作,导致最终并发报错。其中静态方法的原因,将list对应的数据变成了公共变量,不再是私有变量。
最终将list变量继续变成私有变量就能解决这个问题,于是添加了以下代码

public static void insertList(Context context) {
    SerialCloneUtils.deepClone(context);
    new Thread(() -> {
        insert(context);
    });
}

public class SerialCloneUtils {

    /**
     * 使用ObjectStream序列化实现深克隆
     *
     * @return Object obj
     */
    public static <T extends Serializable> T deepClone(T t) {
        InputStream bin = null;
        ObjectInputStream in = null;
        ByteArrayOutputStream bout = null;
        ObjectOutputStream out = null;
        try {
            bout = new ByteArrayOutputStream();
            out = new ObjectOutputStream(bout);
            out.writeObject(t);
            bin = new ByteArrayInputStream(bout.toByteArray());
            in = new ObjectInputStream(bin);
            return (T) (in.readObject());
        } catch (Exception e) {
            logger.error("深克隆对象出现问题,报错信息:" + e.getMessage());
        } finally {
            if (bin != null) {
                try {
                    bin.close();
                } catch (Exception e) {
                    logger.error("深克隆对象, ByteArrayInputStream关闭异常,报错信息:" + e.getMessage());
                }
            }
            if (in != null) {
                try {
                    in.close();
                } catch (Exception e) {
                    logger.error("深克隆对象, ObjectInputStream关闭异常,报错信息:" + e.getMessage());
                }
            }
            if (bout != null) {
                try {
                    bout.close();
                } catch (Exception e) {
                    logger.error("深克隆对象, ByteArrayOutputStream关闭异常,报错信息:" + e.getMessage());
                }
            }
            if (out != null) {
                try {
                    out.close();
                } catch (Exception e) {
                    logger.error("深克隆对象, ObjectOutputStream关闭异常,报错信息:" + e.getMessage());
                }
            }
        }
        return t;
    }

}

完美解决!

以上解决办法会再次出现并发吗?

标签:java,记录,list,并发,报错,context,解决,null
From: https://www.cnblogs.com/xlecho/p/16992149.html

相关文章

  • GEE引擎架设好之后进游戏时白屏的解决方法——gee引擎白屏修复​
    ​这两天测试GeeM2引擎的服务端,最常见的问题就是点击开始游戏出现白屏,最早还以为是服务端问题,结果是因为升级了引擎,而没有升级NewUI这份文件导致的。解决方法如下:​1、下载G......
  • 几种不常见的DNS解析记录类型介绍-中科三方
    一、什么是DNS解析记录?DNS是互联网中一项重要的基础服务,它将简单易记的域名转换成可由计算机识别的IP地址,以便客户端对服务器的正常访问。而由DNS构建起的域名与IP地址之......
  • 并发编程(一)之线程的创建和启动
    并发编程之线程的创建和启动一、线程创建1.1.实现​​Runnable​​接口实现​​Runnable​​​接口,重写​​run​​​方法,实现​​Runnable​​​接口的实现类的实例对象作......
  • jquery.min.map 404 (Not Found)出错的原因及解决办法
    ​​Chrome​​​ 更新后出现了jquery.min.map ​​404​​  (NotFound)的信息这个到底是什么东西?查询了一下,得到了以下资料​​JQuery官方解释​​摘录一下內容从......
  • 常见的内存溢出与解决办法
    引起内存溢出的原因有很多种,常见的有以下几种:1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据;2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;......
  • Android Studio使用Mob实现短信验证功能遇到的问题解决
    一、Mob短信验证​​全球领先的数据智能科技平台-MobTech袤博解决​​进行注册登入 登入成功后,点击开发者服务中的短信验证,进入开发者平台填好信息创建成功后显示下图,可以......
  • 接口文档解决方案之Torna
    ◆一、开源项目简介接口文档解决方案,目标是让接口文档管理变得更加方便、快捷。Torna采用团队协作的方式管理和维护接口文档,将不同形式的文档纳入进来统一维护。Torna弥......
  • 解决警告 gpg WARNING unsafe permissions on homedir
    这里假定我们的GPG文件夹位置为~/.gnupg,只需运行以下几条命令即可解决"gpgWARNINGunsafepermissionsonhomedir"告警信息:chown-R$(whoami)~/.gnupgchmod70......
  • SpringBoot3.x中spring.factories功能被移除的解决方案
    背景笔者所在项目组在搭建一个全新项目的时候选用了​​SpringBoot3.x​​​,项目中应用了很多​​SpringBoot2.x​​​时代相关的第三方组件例如​​baomidou​​​出品的​......
  • 几行代码解决爬虫效果变差问题
    现在的互联网大数据时代中,爬虫ip是网络爬虫不可缺少的一部分。大数据采集最简单直接有效的方法就是使用网络爬虫,不仅速度快,提高了业务率,而且还能更加有效率的采集到数据。网......