先来看3个问题
-
你知道你常用的操作Json的对象是怎么实现的吗?
-
多线程中操作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();
}
}
}
- 多线程中操作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
问题排查
- 首先检查请求日志,请求过来时,日志打印的请求正常,也就是入参肯定是没有问题
- assets对象中的字段变成了null,什么样的操作使它变为null
- 再去查看json源码中opt或者get方法会不会有操作导致取值为null(这里肯定是方法执行前json中是存在值的)
- 知道了org.json底层为HashMap的实现
- 上一步没有问题,再检查有没有put操作
- 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确保线程安全,但不建议。
总结教训
-
尽量不要在多线程中进行json对象的put操作
-
内部调用接口传参也要规范
-
关于一些问题排查的经验教训,一些奇怪的问题,也要有向多线程方面思考的思维