多线程操作Json对象踩坑分享

先来看3个问题
  1. 你知道你常用的操作Json的对象是怎么实现的吗?

  2. 多线程中操作json,进行put操作

public class JsonShare {

    private static volatile AtomicInteger atomCount = new AtomicInteger(0);

    public static void main(String[] args) {
        int count = 0;
        final ExecutorService pool = Executors.newFixedThreadPool(15);
        while (true) {
            final JSONObject asset = new JSONObject();
            asset.put("1", 1);
            asset.put("2", 2);

            pool.execute(() -> assetPut(asset));
            pool.execute(() -> get1AndAdd(asset));
            pool.execute(() -> get2AndAdd(asset));
            if (atomCount.get() > 0) {
                break;
            }
            count++;
            System.out.println(count);
        }
        
        pool.shutdown();
    }

    private static void assetPut(JSONObject asset) {
        asset.put("3", 3);
    }

    private static void get1AndAdd(JSONObject asset) {
        try {
            Integer i = (Integer) asset.get("1");
            // 替代一下其它业务逻辑的操作
            i++;
        } catch (Exception e) {
            atomCount.addAndGet(1);
            e.printStackTrace();
        }

    }

    private static void get2AndAdd(JSONObject asset) {
        try {
            Integer i = (Integer) asset.get("2");
            i++;
        } catch (Exception e) {
            atomCount.addAndGet(1);
            e.printStackTrace();
        }
    }

}
  1. 多线程中操作json,进行put操作
public class JsonShare2 {

    private static volatile AtomicInteger atomCount = new AtomicInteger(0);

    public static void main(String[] args) {
        int count = 0;
        final ExecutorService pool = Executors.newFixedThreadPool(15);
        while (true) {
            final JSONObject asset = new JSONObject();
            for (int i = 1; i <= 12; i++) {
                asset.put(String.valueOf(i), i);
            }

            pool.execute(() -> assetPut(asset));
            pool.execute(() -> get1AndAdd(asset));
            pool.execute(() -> get2AndAdd(asset));
            pool.execute(() -> get3AndAdd(asset));
            pool.execute(() -> get4AndAdd(asset));
            pool.execute(() -> get5AndAdd(asset));
            pool.execute(() -> get6AndAdd(asset));
            pool.execute(() -> get7AndAdd(asset));
            pool.execute(() -> get8AndAdd(asset));
            pool.execute(() -> get9AndAdd(asset));
            pool.execute(() -> get10AndAdd(asset));
            pool.execute(() -> get11AndAdd(asset));
            pool.execute(() -> get12AndAdd(asset));

            if (atomCount.get() > 0) {
                break;
            }
            count++;
            System.out.println(count);
        }

        pool.shutdown();
    }

    private static void assetPut(JSONObject asset) {
        asset.put("13", 13);
    }

    private static void get1AndAdd(JSONObject asset) {
        try {
            Integer i = (Integer) asset.get("1");
            i++;
        } catch (Exception e) {
            atomCount.addAndGet(1);
            e.printStackTrace();
        }

    }

    private static void get2AndAdd(JSONObject asset) {
        try {
            Integer i = (Integer) asset.get("2");
            i++;
        } catch (Exception e) {
            atomCount.addAndGet(1);
            e.printStackTrace();
        }
    }
    
    // 还有类似的方法 这里省略
    .....
}
背景

伽马项目中,负责车300自有规则实现的项目keen,需要并行执行几十大类规则,每类规则都需要操作json,执行完成后再返回给调用方命中的规则以及规则详情。

原始代码:

    public Map<Integer, Map<String, Object>> checkAccidentCar(JSONObject assets) {
        Map<Integer, Map<String, Object>> hitRules = new HashMap<>();
        assets.put("assetsId", assets.optInt("id"));
        /*纯贷后,与贷后的贷前资产类型不一样*/
        assets.put("assetsType", Constants.AssetsType.ACTUAL_POST);
        AccidentCarInfo info = checkAccidentCar(assets, getCheckAfterTime(assets), Constants.PreOrPost.POST);
        ......
        ......
        return hitRules;
    }

checkAccidentCar方法就是并行计算中的一个,类似于getxxxAndAdd方法

问题现象

其他类似checkAccidentCar方法中随机报错npe

问题排查
  1. 首先检查请求日志,请求过来时,日志打印的请求正常,也就是入参肯定是没有问题
  2. assets对象中的字段变成了null,什么样的操作使它变为null
  3. 再去查看json源码中opt或者get方法会不会有操作导致取值为null(这里肯定是方法执行前json中是存在值的)
  4. 知道了org.json底层为HashMap的实现
  5. 上一步没有问题,再检查有没有put操作
  6. put操作中有没有导致json中某个字段为null的操作
反向验证原因

底层原因是找到了,首先的想法是,这个put操作已经在线上运行有一段时间了(接近半年时间),但是这次才暴露出来,为什么呢?

最近一次上线肯定更改了某些内容,导致了现在json进行put操作的时候会进行扩容操作,出现npe

拉出日志中上线前后同一个资产贷后的请求体,我对比了两个请求体的字段个数,终于真相大白。

资产实体类在这个版本增加了一个字段,导致从原来的94个字段,变成了95个字段。keen接收到95个字段的json,在某一个线程中,进行了两次put操作,导致json需要变为97个字段,刚好超过了扩容前的极限值96(128 * 0.75),需要进行一次扩容操作,并行计算量很大的情况下,导致多线程中报错npe

扩容过程分析

resize重灾区

问题的本质回到了大家熟悉的HashMap不能在多线程中操作的问题。

  • Jdk8以前

并发扩容造成死循环,两个节点循环引用。

  • Jdk8以后
    并发扩容不会出现死循环,利用高低位head和tail指针保证链表的顺序与原来一样,但任然是线程不安全的。
解决方法

了解原因之后,解决方法很简单。先前置了json的put操作,紧急上线。

后续思考

org.json版本的底层使用了HashMap,其他版本的json是什么样呢?

  • fastjson版本的底层源码,同样也是HashMap实现
  • jackson版本是LinkedHashMap

不同版本的json对象实现还是不同的,比如org.json初始化可以传入Map对象,但是只取Map的大小,然后初始化HashMap

而fastjson则是使用传入的Map直接初始化进行使用,所以fastjson可以传入一个ConcurrentHashMap确保线程安全,但不建议。

总结教训
  1. 尽量不要在多线程中进行json对象的put操作

  2. 内部调用接口传参也要规范

  3. 关于一些问题排查的经验教训,一些奇怪的问题,也要有向多线程方面思考的思维

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值