對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?)。
蘿蔔白菜各有所愛,這些古老的東西,看起來是有些過時,但不得不說,細細品味其中不乏有大智大慧。