记一次异常 JSONObject toString 出现并发修改
2019-08-18 本文已影响0人
古都旧城
场景描述
做数据上报组件的时候,把jsonObject对象当成了一个方法的参数,由外层传入,而操作此对象的线程可能有多个,比如:上报线程、备份线程等等,然后当数据量大的时候,偶发并发修改异常,经常发现在jsonObject.toString()的时候。
异常
08-13 15:10:54.207 16625-16709/com..xxx E/Tinker.UncaughtHandler: TinkerUncaughtHandler catch exception:java.util.ConcurrentModificationException
at java.util.LinkedHashMap$LinkedHashIterator.nextEntry(LinkedHashMap.java:346)
at java.util.LinkedHashMap$EntryIterator.next(LinkedHashMap.java:375)
at java.util.LinkedHashMap$EntryIterator.next(LinkedHashMap.java:373)
at org.json.JSONObject.writeTo(JSONObject.java:719)
at org.json.JSONStringer.value(JSONStringer.java:237)
at org.json.JSONObject.writeTo(JSONObject.java:720)
at org.json.JSONStringer.value(JSONStringer.java:237)
at org.json.JSONArray.writeTo(JSONArray.java:613)
at org.json.JSONArray.toString(JSONArray.java:585)
at com.xxx.xxx.report.thread.CommonBackupThread.innerBackup(CommonBackupThread.java:36)
at com.xxx.xxx.report.thread.CommonBackupThread.access$000(CommonBackupThread.java:22)
at com.xxx.xxx.report.thread.CommonBackupThread$1.run(CommonBackupThread.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
at java.lang.Thread.run(Thread.java:818)
从上面可以看出,我们在调用JSONArray对象的toString()方法的时候抛出了并发修改异常。
问题分析
什么是并发修改异常
这个异常一般我们还都比较熟悉,大多数场景一般出现在对集合的操作上边:一边迭代一边对集合进行修改,就很容易发生并发修改异常。
例如下面这个实例就会出现并发修改异常
);
list.add(2);
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer s = iterator.next();
if (s == 2) {
list.remove(s);
}
}
具体抛出并发异常的代码分析可以参见:https://www.cnblogs.com/bsjl/p/7676209.html
我们主要分析为何jsonObject 或者 jsonArray toString会抛出异常
首先看jsonObject的toString 方法
@Override public String toString() {
try {
JSONStringer stringer = new JSONStringer();
writeTo(stringer);
return stringer.toString();
} catch (JSONException e) {
return null;
}
}
进入writeTo 方法,可以看到是有一个map集合nameValuePairs
的遍历
void writeTo(JSONStringer stringer) throws JSONException {
stringer.object();
for (Map.Entry<String, Object> entry : nameValuePairs.entrySet()) {
stringer.key(entry.getKey()).value(entry.getValue());
}
stringer.endObject();
}
然后在我们像jsonObject塞入数据的时候是这样的
public JSONObject put(String name, boolean value) throws JSONException {
nameValuePairs.put(checkName(name), value);
return this;
}
看到这里结论就很明确了,toString有调用集合的高级for循环(底层迭代器实现的),然后添加数据的时候又会直接操作集合,读写又不在同一个线程,自然就容易出问题。
异常发生原因回顾
- 1、外层将上报的数据封装成了一个jsonObject 传递给了上报组件(组件内涉及分发、备份等线程)
- 2、组件内的备份线程调用了jsonObject的toString()方法,此方法涉及集合遍历。
- 3、于此同时外层依然在修改此对象用作其他用途,对象修改也涉及到了集合的修改。
- 上面两个动作非同一线程串行的,而是不同线程并发操作,从而触发了集合的并发修改异常,导致崩溃。
总结
- 多线程操作任何对象都容易出问题,不仅仅是jsonObject,对象可能同时存在读写情况的话,不应当作为参数在多个线程之间来回传递,这样是不安全的,也是低级的错误。
- 修改方案:这里场景是上报组件的一个参数,加锁显然是不合理的,这里我们应当保证多个场景使用的对象不是同一个(当然要具体场景具体分析,我们这里是上报数据,数据传入之后,我们无需关心外层对此对象如何更改的,所以可以直接通过创建一个新的对象避免此种情况发生)