java.util.ConcurrentModificationException - ошибка при выполнении операций ввода-вывода (не со списком)

Хорошо. Вот сценарий. У меня есть форма, которую пользователь заполняет для создания объекта Match. Я использую IntentService записать информацию в файл в фоновом потоке. Если boolean "Истина" передается в intentтогда соответствующий 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);

        }
    }

Вот два метода написания файла соответствия и файла результатов:

Файл соответствия:

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 объекты.

Вот класс соответствия:

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 указывает мне на фразу: "jsonString = new Gson().toJson(match);" в WriteMatchToFile(Context, 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$1.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

2 ответа

Решение

Как вы упомянули, Match объект содержит List<Stage>

Объекты соответствия содержат список объектов Stage.

Проблема в том, что пока Gson#toJson() обрабатывает ваш Match объект и преобразовать его в JSON, он также должен перебирать List<Stage> используя итератор и обрабатывать их. Иногда (в случае сбоя вашего приложения) Gson использует итератор для процесса преобразования, а другой поток изменяет List<Stage> как это бывает вложенное поле синглтона. Итератор next() метод, который вызывается в методе Гсона ConcurrentModificationException если список был изменен, так как итератор был получен из списка.

РЕШЕНИЕ
То, как вы хотите справиться с этой ошибкой, зависит от характера вашего приложения (как файлы сохраняются).

Одним из решений является использование CopyOnWriteArrayList для хранения Stage объекты в вашем Match объект. Он использует копию списка для Iterator, Никакие изменения в фактическом списке не влияют на Iterator,

Еще можно synchronise ваши методы, которые пишут Match возражать против файлов и изменять их.

См. Этот пост для получения дополнительной информации о ConcurrentModificationException и как их избежать.

Я хочу поделиться тем, что я понял об этой проблеме, для будущих посетителей этого поста. Спасибо @Pranjal и @Ridcully за указание в правильном направлении. Я хотел бы отдать должное вам обоим.

Как я уже сказал, у меня был глобальный Match объект (синглтон), который содержал List<Stage> как поле Match объект. Хотя на Stage формы, я бы добавил вновь созданный этап после того, как пользователь нажимает кнопку "Сохранить", к List<Stage> объекты глобального Match, Затем я отправил бы это Match для IntentService (фоновый поток), который проанализировал Match возражать в JSON объект с помощью Gson библиотека и написал String в файл.

Пока эта запись происходила в фоновом режиме, пользователь отправляется Fragment который состоит из RecyclerView который содержит список Stage объекты для текущего "глобального" Match, Таким образом, файл все еще писал Match возражать иногда, когда я пытался извлечь его List<String> объекты для списка на следующем экране. Это где ConcurrentModificationError заходи.

2 решения, которые я использовал

1. Я решил разобрать глобальный Match в JSON объект, а затем отправить String в фоновый сервис, чтобы написать String представление Match к File в то время как пользователь взаимодействует со следующим экраном (список Stages в синглтоне). Это сработало, но добавило сложности 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;
            }

Надеюсь, что это поможет кому-то еще, и спасибо всем, кто помог мне.

Другие вопросы по тегам