java.util.ConcurrentModificationException -- 执行 IO 操作时的错误(不使用列表)
java.util.ConcurrentModificationException -- bug when performing IO operations (not with a list)
好的。这是场景。我有一个表单,用户填写该表单以创建一个 Match 对象。我使用 IntentService
在后台线程上将信息写入文件。如果在 intent
中传递 "true" 的 boolean
,则也会写入相应的 ScoreFile
。 GlobalMatch
是单例对象 这里是 IntentService
代码:
public class WriteMatchService extends IntentService {
private static final String EXTRA_SCORES_FILE = "scores_file";
public static final String REFRESH_MATCH_LIST_INTENT_FILTER = "refresh_match_list";
public static Intent getIntent(Context context, boolean writeScoresFile) {
Intent intent = new Intent(context.getApplicationContext(),
WriteMatchService.class);
intent.putExtra(EXTRA_SCORES_FILE, writeScoresFile);
return intent;
}
public WriteMatchService() {
super("WriteMatchService");
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
boolean writeScoresFile = false;
if (intent != null) {
if (intent.hasExtra(EXTRA_SCORES_FILE)) {
writeScoresFile = intent.getBooleanExtra(EXTRA_SCORES_FILE, false);
}
} else {
// this should never happen
return;
}
Context context = getApplicationContext();
// globalMatch is a singleton object
GlobalMatch globalMatch = GlobalMatch.getInstance(context);
Match match = globalMatch.getCurrentMatch();
if (writeScoresFile) {
FileUtils.writeScoresFile(context, new ScoresFile(match.getMatchId()));
}
FileUtils.writeMatchToFile(context, match);
// notify the match list to reload its contents if necessary
Intent messageIntent = new Intent(REFRESH_MATCH_LIST_INTENT_FILTER);
LocalBroadcastManager manager = LocalBroadcastManager.getInstance(getApplicationContext());
manager.sendBroadcast(messageIntent);
}
}
Match文件和Scores文件的两种写法:
匹配文件:
public static void writeMatchToFile(Context context, Match match) {
File file = new File(context.getFilesDir(), Match.getFileName(match.getMatchId().toString()));
String jsonString = "";
FileOutputStream fos = null;
jsonString = new Gson().toJson(match);
try {
fos = context.openFileOutput(file.getName(), Context.MODE_PRIVATE);
fos.write(jsonString.getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
// after creating match, make it the current one
SharedPreferences.Editor editor = context.getSharedPreferences(MY_GLOBAL_MATCH,
Context.MODE_PRIVATE).edit();
editor.putString(CURRENT_MATCH, jsonString);
editor.apply();
}
匹配对象包含 Stage
个对象的列表。
这是比赛 Class:
public class Match implements Parcelable {
private static final String TAG = "Match";
private UUID mMatchId;
private String mClubId;
private String mMatchName;
private Date mMatchDate;
private MatchLevel mMatchLevel;
private List<Stage> mStages;
private List<Competitor> mCompetitors;
private String mPassword;
private MatchType mMatchType;
public enum MatchType {
USPSA("USPSA"), ACTION_STEEL("Action Steel");
private String name;
MatchType(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
private enum MatchLevel {
I("I"), II("II"), III("III"), IV("IV"), V("V");
private String value;
MatchLevel(String value){
this.value = value;
}
@Override
public String toString() {
return value;
}
}
// no arg constructor, most likely to be used as initial match created upon installation
public Match() {
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
mMatchDate = calendar.getTime();
mMatchType = MatchType.USPSA;
mMatchId = UUID.randomUUID();
mMatchName = "";
mMatchLevel = MatchLevel.I;
mClubId = "";
mStages = new ArrayList<>();
mCompetitors = new ArrayList<>();
mPassword = "";
}
public Match(String matchName, Date matchDate, MatchLevel matchLevel, MatchType matchType, String clubID, String password) {
mMatchId = UUID.randomUUID();
mClubId = clubID;
mMatchName = matchName;
mMatchDate = matchDate;
mMatchLevel = matchLevel;
mMatchType = matchType;
mStages = new ArrayList<>();
mCompetitors = new ArrayList<>();
mPassword = password;
}
public String getPassword() {
return mPassword;
}
public void setPassword(String password) {
mPassword = password;
}
public UUID getMatchId() {
return mMatchId;
}
public String getClubId() {
return mClubId;
}
public void setClubId(String clubId) {
mClubId = clubId;
}
public String getMatchName() {
return mMatchName;
}
public void setMatchName(String matchName) {
mMatchName = matchName;
}
public Date getMatchDate() {
return mMatchDate;
}
public void setMatchDate(Date matchDate) {
mMatchDate = matchDate;
}
public MatchLevel getMatchLevel() {
return mMatchLevel;
}
public String getMatchLevelString() {
return mMatchLevel.toString();
}
public void setMatchLevel(MatchLevel matchLevel) {
mMatchLevel = matchLevel;
}
public void setMatchLevel(String str){
switch (str){
case "I":
mMatchLevel = MatchLevel.I;
break;
case "II":
mMatchLevel = MatchLevel.II;
break;
case "III":
mMatchLevel = MatchLevel.III;
break;
case "IV":
mMatchLevel = MatchLevel.IV;
break;
case "V":
mMatchLevel = MatchLevel.V;
break;
default:
Log.d(TAG, "Something went wrong");
}
}
public MatchType getMatchType() {
return mMatchType;
}
public static MatchType getMatchTypeFromString(String matchType){
switch (matchType){
case "USPSA":
return MatchType.USPSA;
case "Action Steel":
return MatchType.ACTION_STEEL;
default:
return null;
}
}
public String getMatchTypeString(){
return mMatchType.toString();
}
public void setMatchType(MatchType matchType){ mMatchType = matchType;}
public void setMatchType(String matchType) {
switch (matchType){
case "USPSA":
mMatchType = MatchType.USPSA;
break;
case "Action Steel":
mMatchType = MatchType.ACTION_STEEL;
break;
default:
Log.d(TAG, "Something went wrong");
}
}
public void addStage(Stage stage) {
mStages.add(stage);
}
public List<Stage> getStages() {
return mStages;
}
public void setStages(List<Stage> stages) {
mStages = stages;
}
public List<Competitor> getCompetitors() {
return mCompetitors;
}
public void setCompetitors(List<Competitor> competitors) {
mCompetitors = competitors;
}
public void addCompetitor(Competitor competitor) {
// if adding a competitor here, assign the competitor a shooter number
competitor.setShooterNum(mCompetitors.size() + 1);
mCompetitors.add(competitor);
}
public void updateCompetitorInMatch(Competitor comp) {
for (Competitor c : mCompetitors) {
// this works because both will have the same ID
if (c.equals(comp) && (c.getShooterNum() == comp.getShooterNum())) {
mCompetitors.remove(c);
mCompetitors.add(comp);
break;
}
}
}
public void updateStageInMatch(Stage stage){
for (Stage s : mStages){
if(stage.equals(s)){
mStages.remove(s);
mStages.add(stage);
break;
}
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Match match = (Match) o;
return Objects.equals(mMatchId, match.mMatchId);
}
@Override
public int hashCode() {
return 7 * mMatchId.hashCode();
}
public static String getFileName(String matchID) {
return "match." + matchID + ".json";
}
public static String formatMatchDate(Date date){
return (String) DateFormat.format("MM/dd/yyyy", date);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeSerializable(this.mMatchId);
dest.writeString(this.mClubId);
dest.writeString(this.mMatchName);
dest.writeLong(this.mMatchDate != null ? this.mMatchDate.getTime() : -1);
dest.writeInt(this.mMatchLevel == null ? -1 : this.mMatchLevel.ordinal());
dest.writeTypedList(this.mStages);
dest.writeTypedList(this.mCompetitors);
dest.writeString(this.mPassword);
dest.writeInt(this.mMatchType == null ? -1 : this.mMatchType.ordinal());
}
protected Match(Parcel in) {
this.mMatchId = (UUID) in.readSerializable();
this.mClubId = in.readString();
this.mMatchName = in.readString();
long tmpMMatchDate = in.readLong();
this.mMatchDate = tmpMMatchDate == -1 ? null : new Date(tmpMMatchDate);
int tmpMMatchLevel = in.readInt();
this.mMatchLevel = tmpMMatchLevel == -1 ? null : MatchLevel.values()[tmpMMatchLevel];
this.mStages = in.createTypedArrayList(Stage.CREATOR);
this.mCompetitors = in.createTypedArrayList(Competitor.CREATOR);
this.mPassword = in.readString();
int tmpMMatchType = in.readInt();
this.mMatchType = tmpMMatchType == -1 ? null : MatchType.values()[tmpMMatchType];
}
public static final Creator<Match> CREATOR = new Creator<Match>() {
@Override
public Match createFromParcel(Parcel source) {
return new Match(source);
}
@Override
public Match[] newArray(int size) {
return new Match[size];
}
};
}
这是 Stage
表单文件的一部分,我在其中收集字段并创建 IntentService:
{
mStage.setTime(Double.valueOf(mTime.getText().toString()));
mStage.setScoringType(Stage.getScoringTypeFromString(mScoringTypeSpinner.getSelectedItem().toString()));
mStage.setSteelTargets(Integer.valueOf(mSteelTargets.getText().toString()));
mStage.setSteelNPMs(Integer.valueOf(mSteelNPMs.getText().toString()));
mStage.setNoShoots(mNoShoots.isChecked());
mStage.setRounds(mStage.getNumberOfSteelTargets());
mStage.setPoints(mStage.getNumberOfSteelTargets() * 5);
// if creating a new stage include a stage number in the stage name
if (!mEditing) {
int stageNum = (mMatch.getStages().size() + 1);
mStage.setStageNum(stageNum);
mStage.setStageName("Stage " + stageNum + ": " + mStageName.getText().toString());
mMatch.addStage(mStage);
Intent serviceIntent = WriteMatchService.getIntent(mContext, false);
mContext.startService(serviceIntent);
} else if (mEditing) {
// if editing an existing stage, only edit the part of the name after the stage number
mStage.setStageName(getNamePrefix(mStage.getStageName()) + mStageName.getText().toString());
mMatch.updateStageInMatch(mStage);
Intent serviceIntent = WriteMatchService.getIntent(mContext, false);
mContext.startService(serviceIntent);
}
}
服务启动后,我将用户发送到另一个片段。注意:该服务应该 运行 相当快。我一直在收到 java.util.ConcurrentModificationException、,但只是偶尔收到一次。 并非每次都会发生这种情况。 我无法确定为什么会出现此错误。 Android Studio 将我指向 WriteMatchToFile(Context, Match)
方法中的“jsonString = new Gson().toJson(match);
”行。我错过了什么?
这是显示错误的堆栈跟踪部分:
--------- beginning of crash
2018-11-25 11:20:30.194 13665-13777/com.patgekoski.ez_score E/AndroidRuntime: FATAL EXCEPTION: IntentService[WriteMatchService]
Process: com.patgekoski.ez_score, PID: 13665
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.next(ArrayList.java:860)
at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.write(CollectionTypeAdapterFactory.java:96)
at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.write(CollectionTypeAdapterFactory.java:61)
at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:69)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.write(ReflectiveTypeAdapterFactory.java:127)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:245)
at com.google.gson.Gson.toJson(Gson.java:704)
at com.google.gson.Gson.toJson(Gson.java:683)
at com.google.gson.Gson.toJson(Gson.java:638)
at com.google.gson.Gson.toJson(Gson.java:618)
at com.patgekoski.ez_score.util.FileUtils.writeMatchToFile(FileUtils.java:75)
at com.patgekoski.ez_score.services.WriteMatchService.onHandleIntent(WriteMatchService.java:63)
at android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:76)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:164)
at android.os.HandlerThread.run(HandlerThread.java:65)
2018-11-25 11:20:30.198 1699-13283/system_process W/ActivityManager: Force finishing activity com.patgekoski.ez_score/.StagesListActivity
如您所述,Match
对象包含一个 List<Stage>
Match objects contain a list of Stage objects.
问题是,当 Gson#toJson()
正在处理您的 Match
对象并将其转换为 JSON 时,它还必须使用迭代器迭代 List<Stage>
并处理它们。有时(当您的应用程序失败时),迭代器被 Gson 用于转换过程并且 另一个线程修改 List<Stage>
因为它恰好是 单例。如果自从从列表中获取迭代器以来列表已被修改,则在 Gson 的方法中调用的迭代器的 next()
方法将抛出 ConcurrentModificationException
。
解决方案
您希望如何处理此故障取决于您的应用程序的性质(文件的保存方式)。
其中一个解决方案是使用 CopyOnWriteArrayList
在 Match
对象中存储 Stage
对象。它为 Iterator
使用列表的副本。对实际列表的修改不会影响 Iterator
.
另一个可以是 synchronise
将 Match
对象写入文件并修改它们的方法。
参考 this post 了解更多关于 ConcurrentModificationException
的信息以及如何避免它们。
我想与此 post 的未来访问者分享我对这个问题的理解。感谢@Pranjal 和@Ridcully 为我指明了正确的方向。我希望我能赞扬你们俩。
就像我说的,我有一个全局 Match
对象(单例),其中包含一个 List<Stage>
作为 Match
对象的字段。在 Stage
表单上,我会在用户按下 "Save" 按钮后将新创建的舞台添加到全局 Match
的 List<Stage>
对象中。然后,我会将 Match
发送到 IntentService
(后台线程),该线程使用 Gson
库将 Match
对象解析为 JSON
对象并编写String
到一个文件。
虽然在后台进行此写入,但用户会被发送到一个 Fragment
,该 Fragment
由一个 RecyclerView
组成,该 RecyclerView
包含当前 Stage
对象的列表"global"Match
。因此,当我试图为下一个屏幕上的列表提取 List<String>
对象时,文件有时仍在写入 Match
对象。这就是 ConcurrentModificationError
进来的地方。
我使用了 2 个解决方案
1.我决定把全局的Match
解析成一个JSON
对象,然后把String
发送给后台服务来当用户与下一个屏幕(单例中 Stages
的列表)交互时,将 Match
的 String
表示写入 File
。这行得通,但它增加了 Fragment
代码的复杂性,我想找到一种方法将 Match
对象本身传递给后台服务。所以,我想出了下面的选项 2。
2. 我确定的解决方案是这样的:我在后台服务中克隆了全局 Match
对象。现在,当用户转到下一个屏幕时,他们正在与一个不同的 Object
进行交互,而不是正在写入 File
的那个,但是两者 Objects
具有相同的数据。问题解决了。
对于不懂克隆的人来说,这很简单。我让 Match.class
实现了 Cloneable
接口。然后像这样覆盖方法 clone()
:
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
在后台服务中,我像这样克隆全局 Match
:
if (intent.hasExtra(EXTRA_MATCH)) {
// clone the match object to prevent a concurrentModificationException
Match originalMatch = intent.getParcelableExtra(EXTRA_MATCH);
try {
match = (Match) originalMatch.clone();
FileUtils.writeMatchToFile(context, match);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
scoresFile = intent.getParcelableExtra(EXTRA_SCORES_FILE);
if (scoresFile != null) {
FileUtils.writeScoresFile(context, scoresFile);
}
} else {
return;
}
希望这对其他人有所帮助,感谢所有帮助过我的人。
好的。这是场景。我有一个表单,用户填写该表单以创建一个 Match 对象。我使用 IntentService
在后台线程上将信息写入文件。如果在 intent
中传递 "true" 的 boolean
,则也会写入相应的 ScoreFile
。 GlobalMatch
是单例对象 这里是 IntentService
代码:
public class WriteMatchService extends IntentService {
private static final String EXTRA_SCORES_FILE = "scores_file";
public static final String REFRESH_MATCH_LIST_INTENT_FILTER = "refresh_match_list";
public static Intent getIntent(Context context, boolean writeScoresFile) {
Intent intent = new Intent(context.getApplicationContext(),
WriteMatchService.class);
intent.putExtra(EXTRA_SCORES_FILE, writeScoresFile);
return intent;
}
public WriteMatchService() {
super("WriteMatchService");
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
boolean writeScoresFile = false;
if (intent != null) {
if (intent.hasExtra(EXTRA_SCORES_FILE)) {
writeScoresFile = intent.getBooleanExtra(EXTRA_SCORES_FILE, false);
}
} else {
// this should never happen
return;
}
Context context = getApplicationContext();
// globalMatch is a singleton object
GlobalMatch globalMatch = GlobalMatch.getInstance(context);
Match match = globalMatch.getCurrentMatch();
if (writeScoresFile) {
FileUtils.writeScoresFile(context, new ScoresFile(match.getMatchId()));
}
FileUtils.writeMatchToFile(context, match);
// notify the match list to reload its contents if necessary
Intent messageIntent = new Intent(REFRESH_MATCH_LIST_INTENT_FILTER);
LocalBroadcastManager manager = LocalBroadcastManager.getInstance(getApplicationContext());
manager.sendBroadcast(messageIntent);
}
}
Match文件和Scores文件的两种写法:
匹配文件:
public static void writeMatchToFile(Context context, Match match) {
File file = new File(context.getFilesDir(), Match.getFileName(match.getMatchId().toString()));
String jsonString = "";
FileOutputStream fos = null;
jsonString = new Gson().toJson(match);
try {
fos = context.openFileOutput(file.getName(), Context.MODE_PRIVATE);
fos.write(jsonString.getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
// after creating match, make it the current one
SharedPreferences.Editor editor = context.getSharedPreferences(MY_GLOBAL_MATCH,
Context.MODE_PRIVATE).edit();
editor.putString(CURRENT_MATCH, jsonString);
editor.apply();
}
匹配对象包含 Stage
个对象的列表。
这是比赛 Class:
public class Match implements Parcelable {
private static final String TAG = "Match";
private UUID mMatchId;
private String mClubId;
private String mMatchName;
private Date mMatchDate;
private MatchLevel mMatchLevel;
private List<Stage> mStages;
private List<Competitor> mCompetitors;
private String mPassword;
private MatchType mMatchType;
public enum MatchType {
USPSA("USPSA"), ACTION_STEEL("Action Steel");
private String name;
MatchType(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
private enum MatchLevel {
I("I"), II("II"), III("III"), IV("IV"), V("V");
private String value;
MatchLevel(String value){
this.value = value;
}
@Override
public String toString() {
return value;
}
}
// no arg constructor, most likely to be used as initial match created upon installation
public Match() {
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
mMatchDate = calendar.getTime();
mMatchType = MatchType.USPSA;
mMatchId = UUID.randomUUID();
mMatchName = "";
mMatchLevel = MatchLevel.I;
mClubId = "";
mStages = new ArrayList<>();
mCompetitors = new ArrayList<>();
mPassword = "";
}
public Match(String matchName, Date matchDate, MatchLevel matchLevel, MatchType matchType, String clubID, String password) {
mMatchId = UUID.randomUUID();
mClubId = clubID;
mMatchName = matchName;
mMatchDate = matchDate;
mMatchLevel = matchLevel;
mMatchType = matchType;
mStages = new ArrayList<>();
mCompetitors = new ArrayList<>();
mPassword = password;
}
public String getPassword() {
return mPassword;
}
public void setPassword(String password) {
mPassword = password;
}
public UUID getMatchId() {
return mMatchId;
}
public String getClubId() {
return mClubId;
}
public void setClubId(String clubId) {
mClubId = clubId;
}
public String getMatchName() {
return mMatchName;
}
public void setMatchName(String matchName) {
mMatchName = matchName;
}
public Date getMatchDate() {
return mMatchDate;
}
public void setMatchDate(Date matchDate) {
mMatchDate = matchDate;
}
public MatchLevel getMatchLevel() {
return mMatchLevel;
}
public String getMatchLevelString() {
return mMatchLevel.toString();
}
public void setMatchLevel(MatchLevel matchLevel) {
mMatchLevel = matchLevel;
}
public void setMatchLevel(String str){
switch (str){
case "I":
mMatchLevel = MatchLevel.I;
break;
case "II":
mMatchLevel = MatchLevel.II;
break;
case "III":
mMatchLevel = MatchLevel.III;
break;
case "IV":
mMatchLevel = MatchLevel.IV;
break;
case "V":
mMatchLevel = MatchLevel.V;
break;
default:
Log.d(TAG, "Something went wrong");
}
}
public MatchType getMatchType() {
return mMatchType;
}
public static MatchType getMatchTypeFromString(String matchType){
switch (matchType){
case "USPSA":
return MatchType.USPSA;
case "Action Steel":
return MatchType.ACTION_STEEL;
default:
return null;
}
}
public String getMatchTypeString(){
return mMatchType.toString();
}
public void setMatchType(MatchType matchType){ mMatchType = matchType;}
public void setMatchType(String matchType) {
switch (matchType){
case "USPSA":
mMatchType = MatchType.USPSA;
break;
case "Action Steel":
mMatchType = MatchType.ACTION_STEEL;
break;
default:
Log.d(TAG, "Something went wrong");
}
}
public void addStage(Stage stage) {
mStages.add(stage);
}
public List<Stage> getStages() {
return mStages;
}
public void setStages(List<Stage> stages) {
mStages = stages;
}
public List<Competitor> getCompetitors() {
return mCompetitors;
}
public void setCompetitors(List<Competitor> competitors) {
mCompetitors = competitors;
}
public void addCompetitor(Competitor competitor) {
// if adding a competitor here, assign the competitor a shooter number
competitor.setShooterNum(mCompetitors.size() + 1);
mCompetitors.add(competitor);
}
public void updateCompetitorInMatch(Competitor comp) {
for (Competitor c : mCompetitors) {
// this works because both will have the same ID
if (c.equals(comp) && (c.getShooterNum() == comp.getShooterNum())) {
mCompetitors.remove(c);
mCompetitors.add(comp);
break;
}
}
}
public void updateStageInMatch(Stage stage){
for (Stage s : mStages){
if(stage.equals(s)){
mStages.remove(s);
mStages.add(stage);
break;
}
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Match match = (Match) o;
return Objects.equals(mMatchId, match.mMatchId);
}
@Override
public int hashCode() {
return 7 * mMatchId.hashCode();
}
public static String getFileName(String matchID) {
return "match." + matchID + ".json";
}
public static String formatMatchDate(Date date){
return (String) DateFormat.format("MM/dd/yyyy", date);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeSerializable(this.mMatchId);
dest.writeString(this.mClubId);
dest.writeString(this.mMatchName);
dest.writeLong(this.mMatchDate != null ? this.mMatchDate.getTime() : -1);
dest.writeInt(this.mMatchLevel == null ? -1 : this.mMatchLevel.ordinal());
dest.writeTypedList(this.mStages);
dest.writeTypedList(this.mCompetitors);
dest.writeString(this.mPassword);
dest.writeInt(this.mMatchType == null ? -1 : this.mMatchType.ordinal());
}
protected Match(Parcel in) {
this.mMatchId = (UUID) in.readSerializable();
this.mClubId = in.readString();
this.mMatchName = in.readString();
long tmpMMatchDate = in.readLong();
this.mMatchDate = tmpMMatchDate == -1 ? null : new Date(tmpMMatchDate);
int tmpMMatchLevel = in.readInt();
this.mMatchLevel = tmpMMatchLevel == -1 ? null : MatchLevel.values()[tmpMMatchLevel];
this.mStages = in.createTypedArrayList(Stage.CREATOR);
this.mCompetitors = in.createTypedArrayList(Competitor.CREATOR);
this.mPassword = in.readString();
int tmpMMatchType = in.readInt();
this.mMatchType = tmpMMatchType == -1 ? null : MatchType.values()[tmpMMatchType];
}
public static final Creator<Match> CREATOR = new Creator<Match>() {
@Override
public Match createFromParcel(Parcel source) {
return new Match(source);
}
@Override
public Match[] newArray(int size) {
return new Match[size];
}
};
}
这是 Stage
表单文件的一部分,我在其中收集字段并创建 IntentService:
{
mStage.setTime(Double.valueOf(mTime.getText().toString()));
mStage.setScoringType(Stage.getScoringTypeFromString(mScoringTypeSpinner.getSelectedItem().toString()));
mStage.setSteelTargets(Integer.valueOf(mSteelTargets.getText().toString()));
mStage.setSteelNPMs(Integer.valueOf(mSteelNPMs.getText().toString()));
mStage.setNoShoots(mNoShoots.isChecked());
mStage.setRounds(mStage.getNumberOfSteelTargets());
mStage.setPoints(mStage.getNumberOfSteelTargets() * 5);
// if creating a new stage include a stage number in the stage name
if (!mEditing) {
int stageNum = (mMatch.getStages().size() + 1);
mStage.setStageNum(stageNum);
mStage.setStageName("Stage " + stageNum + ": " + mStageName.getText().toString());
mMatch.addStage(mStage);
Intent serviceIntent = WriteMatchService.getIntent(mContext, false);
mContext.startService(serviceIntent);
} else if (mEditing) {
// if editing an existing stage, only edit the part of the name after the stage number
mStage.setStageName(getNamePrefix(mStage.getStageName()) + mStageName.getText().toString());
mMatch.updateStageInMatch(mStage);
Intent serviceIntent = WriteMatchService.getIntent(mContext, false);
mContext.startService(serviceIntent);
}
}
服务启动后,我将用户发送到另一个片段。注意:该服务应该 运行 相当快。我一直在收到 java.util.ConcurrentModificationException、,但只是偶尔收到一次。 并非每次都会发生这种情况。 我无法确定为什么会出现此错误。 Android Studio 将我指向 WriteMatchToFile(Context, Match)
方法中的“jsonString = new Gson().toJson(match);
”行。我错过了什么?
这是显示错误的堆栈跟踪部分:
--------- beginning of crash
2018-11-25 11:20:30.194 13665-13777/com.patgekoski.ez_score E/AndroidRuntime: FATAL EXCEPTION: IntentService[WriteMatchService]
Process: com.patgekoski.ez_score, PID: 13665
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.next(ArrayList.java:860)
at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.write(CollectionTypeAdapterFactory.java:96)
at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.write(CollectionTypeAdapterFactory.java:61)
at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:69)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.write(ReflectiveTypeAdapterFactory.java:127)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:245)
at com.google.gson.Gson.toJson(Gson.java:704)
at com.google.gson.Gson.toJson(Gson.java:683)
at com.google.gson.Gson.toJson(Gson.java:638)
at com.google.gson.Gson.toJson(Gson.java:618)
at com.patgekoski.ez_score.util.FileUtils.writeMatchToFile(FileUtils.java:75)
at com.patgekoski.ez_score.services.WriteMatchService.onHandleIntent(WriteMatchService.java:63)
at android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:76)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:164)
at android.os.HandlerThread.run(HandlerThread.java:65)
2018-11-25 11:20:30.198 1699-13283/system_process W/ActivityManager: Force finishing activity com.patgekoski.ez_score/.StagesListActivity
如您所述,Match
对象包含一个 List<Stage>
Match objects contain a list of Stage objects.
问题是,当 Gson#toJson()
正在处理您的 Match
对象并将其转换为 JSON 时,它还必须使用迭代器迭代 List<Stage>
并处理它们。有时(当您的应用程序失败时),迭代器被 Gson 用于转换过程并且 另一个线程修改 List<Stage>
因为它恰好是 单例。如果自从从列表中获取迭代器以来列表已被修改,则在 Gson 的方法中调用的迭代器的 next()
方法将抛出 ConcurrentModificationException
。
解决方案
您希望如何处理此故障取决于您的应用程序的性质(文件的保存方式)。
其中一个解决方案是使用 CopyOnWriteArrayList
在 Match
对象中存储 Stage
对象。它为 Iterator
使用列表的副本。对实际列表的修改不会影响 Iterator
.
另一个可以是 synchronise
将 Match
对象写入文件并修改它们的方法。
参考 this post 了解更多关于 ConcurrentModificationException
的信息以及如何避免它们。
我想与此 post 的未来访问者分享我对这个问题的理解。感谢@Pranjal 和@Ridcully 为我指明了正确的方向。我希望我能赞扬你们俩。
就像我说的,我有一个全局 Match
对象(单例),其中包含一个 List<Stage>
作为 Match
对象的字段。在 Stage
表单上,我会在用户按下 "Save" 按钮后将新创建的舞台添加到全局 Match
的 List<Stage>
对象中。然后,我会将 Match
发送到 IntentService
(后台线程),该线程使用 Gson
库将 Match
对象解析为 JSON
对象并编写String
到一个文件。
虽然在后台进行此写入,但用户会被发送到一个 Fragment
,该 Fragment
由一个 RecyclerView
组成,该 RecyclerView
包含当前 Stage
对象的列表"global"Match
。因此,当我试图为下一个屏幕上的列表提取 List<String>
对象时,文件有时仍在写入 Match
对象。这就是 ConcurrentModificationError
进来的地方。
我使用了 2 个解决方案
1.我决定把全局的Match
解析成一个JSON
对象,然后把String
发送给后台服务来当用户与下一个屏幕(单例中 Stages
的列表)交互时,将 Match
的 String
表示写入 File
。这行得通,但它增加了 Fragment
代码的复杂性,我想找到一种方法将 Match
对象本身传递给后台服务。所以,我想出了下面的选项 2。
2. 我确定的解决方案是这样的:我在后台服务中克隆了全局 Match
对象。现在,当用户转到下一个屏幕时,他们正在与一个不同的 Object
进行交互,而不是正在写入 File
的那个,但是两者 Objects
具有相同的数据。问题解决了。
对于不懂克隆的人来说,这很简单。我让 Match.class
实现了 Cloneable
接口。然后像这样覆盖方法 clone()
:
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
在后台服务中,我像这样克隆全局 Match
:
if (intent.hasExtra(EXTRA_MATCH)) {
// clone the match object to prevent a concurrentModificationException
Match originalMatch = intent.getParcelableExtra(EXTRA_MATCH);
try {
match = (Match) originalMatch.clone();
FileUtils.writeMatchToFile(context, match);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
scoresFile = intent.getParcelableExtra(EXTRA_SCORES_FILE);
if (scoresFile != null) {
FileUtils.writeScoresFile(context, scoresFile);
}
} else {
return;
}
希望这对其他人有所帮助,感谢所有帮助过我的人。