对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:    * 
{"query":"Pizza","locations":[94043,90210]}
*/
@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?)。

萝卜白菜各有所爱,这些古老的东西,看起来是有些过时,但不得不说,细细品味其中不乏有大智大慧。

更多相关文章

  1. NDK C++线程中如何调用JAVA API
  2. android 5.0之后利用Intent传递Serializable对象存在问题
  3. 【摘录】Android的线程使用来更新UI----Thread、Handler、Looper
  4. android 多线程编程
  5. Android 学习系列 - 线程模型

随机推荐

  1. Android 学习系列 - Java 多线程
  2. Android 返回键实现home键的功能
  3. Android中PopupWindow的使用
  4. android 音乐播放工具类MediaPlayer
  5. android上方显示进度的进度条
  6. Android ImageLoader组件加载图片
  7. android 开发小组
  8. APK Signature
  9. Android(安卓)面试 100 题
  10. Android 获取imu数据