Android中访问MutableLiveData对象时出现ConcurrentModificationException排查
异常原因
java.util.ConcurrentModificationException 触发的核心是多线程环境下对同一列表的并发访问与修改冲突,结合你的代码场景具体有两点:
ArrayList迭代器的快速失败机制
ArrayList的迭代器采用快速失败(fail-fast)设计:迭代器创建后,若列表结构(元素数量)被修改,或多线程中一个线程遍历、另一个线程修改列表,迭代器在执行next()时会检测到列表的modCount(修改次数)与迭代器内部的expectedModCount不一致,直接抛出异常。你的代码中,虽然
set()方法不会改变列表大小,但遍历Answer列表后,你执行了postValue(exam)和updateQuestion(1),这两个操作可能触发其他线程(比如LiveData观察者线程、主线程UI更新逻辑)访问并修改同一个Exam/Question/Answer列表,和当前遍历操作产生竞态条件,触发异常。冗余操作增加并发风险
遍历逻辑里的exam.getQuestions().get(questionPosition).getAnswers().set(indexAnswer, answer)完全多余:Answer是引用类型,通过iterator.next()拿到的就是列表中的对象引用,直接调用answer.setAnswer(...)会同步修改列表内的元素,无需再调用set()放回列表。这个冗余操作虽不直接修改列表结构,但在多线程场景下会增加竞态条件的触发概率。
解决方案
针对你的需求,从避免并发修改和规范LiveData使用两个方向调整代码:
1. 移除冗余操作,简化遍历逻辑
直接修改Answer对象属性即可,无需调用列表set()方法:
Iterator<Answer> iterator = exam.getQuestions().get(questionPosition).getAnswers().iterator(); while (iterator.hasNext()) { Answer answer = iterator.next(); // 直接修改对象属性,无需放回列表 answer.setAnswer(answer.getId() == checkedId); }
2. 使用深副本修改后再post,避免共享对象的并发冲突
LiveData的最佳实践是不直接修改其持有的对象实例,而是创建对象的深副本,修改副本后再post新实例,确保观察者拿到的是独立对象,不会和修改操作冲突:
// 1. 创建Exam的深副本 Exam newExam = new Exam(); List<Question> newQuestions = new ArrayList<>(); for (Question q : exam.getQuestions()) { Question newQ = new Question(); newQ.setText(q.getText()); newQ.setIndex(q.getIndex()); // 拷贝Answer列表 List<Answer> newAnswers = new ArrayList<>(); for (Answer a : q.getAnswers()) { Answer newA = new Answer(); newA.setId(a.getId()); newA.setAnswer(a.isAnswer()); newAnswers.add(newA); } newQ.setAnswers(newAnswers); newQuestions.add(newQ); } newExam.setQuestions(newQuestions); // 2. 修改副本中的Answer List<Answer> targetAnswers = newExam.getQuestions().get(questionPosition).getAnswers(); for (Answer answer : targetAnswers) { answer.setAnswer(answer.getId() == checkedId); } // 3. 提交新副本到LiveData questionViewModel.getExam().postValue(newExam);
3. 封装ViewModel内的修改逻辑,避免外部直接操作LiveData对象
在ViewModel中封装Answer修改逻辑,避免Fragment直接操作LiveData持有的实例,进一步降低并发风险:
// ViewModel内新增方法 public void updateAnswer(int questionPos, int checkedId) { Exam currentExam = liveExam.getValue(); if (currentExam == null) return; // 创建深副本并修改 Exam newExam = createDeepCopy(currentExam); List<Answer> answers = newExam.getQuestions().get(questionPos).getAnswers(); for (Answer answer : answers) { answer.setAnswer(answer.getId() == checkedId); } liveExam.postValue(newExam); } // 封装深拷贝逻辑 private Exam createDeepCopy(Exam original) { Exam copy = new Exam(); List<Question> questionCopies = new ArrayList<>(); for (Question q : original.getQuestions()) { Question qCopy = new Question(); qCopy.setText(q.getText()); qCopy.setIndex(q.getIndex()); List<Answer> answerCopies = new ArrayList<>(); for (Answer a : q.getAnswers()) { Answer aCopy = new Answer(); aCopy.setId(a.getId()); aCopy.setAnswer(a.isAnswer()); answerCopies.add(aCopy); } qCopy.setAnswers(answerCopies); questionCopies.add(qCopy); } copy.setQuestions(questionCopies); return copy; }
Fragment中直接调用ViewModel方法即可:
questionViewModel.updateAnswer(questionPosition, checkedId); try { questionViewModel.updateQuestion(1); } catch (Exception e) { e.printStackTrace(); } onUpdateQuestion();
内容的提问来源于stack exchange,提问作者Farhood Naqizadeh

