对JSONObject抛ConcurrentModificationException的一点思考
问题背景
NetUtil.get(url, arg, new NetUtil.Callback() {
@WorkerThread
@Override
public void onSucceed(JSONObject repose) {
}
@WorkerThread
@Override
public void onFailed(String msg, Throwable t) {
try {
JSONObject event = new JSONObject();
event.put("event_name", "request_error");
event.put("error_msg", String.valueOf(msg));
event.put("stacktrace", null == t ? "" : Log.getStackTraceString(t));
// 上报请求失败
statics(event);
// log显示失败详情,
// 此处抛ConcurrentModificationException
Log.e(TAG, "request product list error: " + event);
} catch (JSONException e) {
e.printStackTrace();
}
}
});
// 打点上报
private void statics(final JSONObject event) {
ThreadUtil.bi().submit(new Runnable() {
@Override
public void run() {
// 添加公共参数
event.put("__client_ms", System.currentTimeMillis());
event.put("__client_ver", BuildConfig.VERSION_CODE);
// 上报
report(event);
}
});
}
这看起来只有局部变量,好像没什么问题…
问题原因
ConcurrentModificationException
是一个并发读写的问题,发生在不同线程的读写一个线程不安全的集合时。
以上代码,看似没有问题,而且也没看到有显式的线程不安全的集合。
其实不安全的集合是有的 JSONObject
:在Log.e(TAG, "request product list error: " + event);
中,隐藏了一个toString的语法糖: event.toString()
;跟进去以后是这样的:
public class JSONObject {
/**
* Encodes this object as a compact JSON string, such as:
* <pre>{"query":"Pizza","locations":[94043,90210]}</pre>
*/
@Override public String toString() {
try {
JSONStringer stringer = new JSONStringer();
writeTo(stringer);
return stringer.toString();
} catch (JSONException e) {
return null;
}
}
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();
}
}
可以看到,这里有个nameValuePairs
的全局变量,它的声明是:
public class JSONObject {
private final LinkedHashMap<String, Object> nameValuePairs;
}
再来看,背景中的两行代码:
// 根据上面的分析,这里有个添加公共参数的写操作。
statics(event);
// 根据上面的分析,这里是一个读操作。
Log.e(TAG, "request product list error: " + event);
到这里就清晰了,
读写不再同一个线程,又存在对象作用域拓展的问题,自然有概率发生Crash。
问题解决
// 打点上报
private void statics(JSONObject event) {
// 重新创建一个对象
final eventObject = new JSONObject(event.toString());
ThreadUtil.bi().submit(new Runnable() {
@Override
public void run() {
// 添加公共参数
eventObject.put("__client_ms", System.currentTimeMillis());
eventObject.put("__client_ver", BuildConfig.VERSION_CODE);
// 上报
report(eventObject);
}
});
}
问题覆盘
上面的问题,说复杂其实不复杂,一般有一些多线程相关的基本功都可以理解,但确实有些隐秘。
上报是一个很常见的案例,经过抽象抽象,代码大同小异,模型线程模型基本可如上述所示。这里想要反思的并不是埋点,亦或是解Crash,并发的问题一般不是必现问题,多半靠脑补。这里值得一提的,是迪米特法则 和 耦合程度。(可参考:https://blog.csdn.net/Fantastic_/article/details/84501992 )
很多人喜欢利用引用传参,然后内部修改属性,进而达到值返回的效果。其实仔细想想,这样拓宽了对象的作用域,延长了生命周期。不论是从软件耦合的角度,或者是设计模式的角度,其实这是有欠妥当的做法。或许笔者这么说会引来很多不屑(比如RxJava?)。
萝卜白菜各有所爱,这些古老的东西,看起来是有些过时,但不得不说,细细品味其中不乏有大智大慧。