Java - SwingWorker - Как использовать массив из публикации в EDT

Как избежать java.util.ConcurrentModificationException при доступе к списку, который обрабатывается в другом потоке (SwingWorker)?

Подробности о том, что я пытаюсь использовать:

  • класс GUI, который содержит этот «основной» метод, и я думаю, что он должен работать в EDT.

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                new myGUIwithButton();
            }
        });
    }
    
  • Этот графический интерфейс имеет метод рисования, который берет список Words, класса, содержащего строки и координаты, и показывает их:

    public void paint(final List<Word> words){
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                // prepare GUI's background, then :
                for(Word word : words){ // <-- this is line 170 in the error shown below
                    // show each word in the GUI
                    // note that I'm not modifying the words list here
                }
    
  • При нажатии кнопки в графическом интерфейсе выполняется экземпляр класса, расширяющего SwingWorker<List<Word>,List<Word>>. Это создает рабочий поток, насколько я понимаю. Переопределенный метод doInBackground создает список Word, а затем регулярно его публикует:

    public List<Word> doInBackground() {
        List<Word> words = new ArrayList<Word>();
        while (!isCancelled()) {
            // do some work, define aNewWord
            words.add( aNewWord );
            publish(words);
            Thread.pause(someTime);
        }
        return words;
    }
    
  • Затем опубликованные слова автоматически отправляются в переопределенный метод process:

    protected void process(List< List<Word> > wordss) { // Executed on EDT ! <3
        // I'm taking only the first list that was published to avoid trouble
        List<Word> words = wordss.get(0); 
        myGUI.paint(words);
    }
    

В чем проблема?

Когда рабочий поток идет «быстро» (пауза менее 50 мс), я часто получаю исключение для строки 170, которая находится в методе paint (файл GUI называется MotsFleches.java):

Exception in thread "AWT-EventQueue-1" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at MotsFleches$2.run(MotsFleches.java:170)
at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:311)
at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:756)
at java.awt.EventQueue.access$500(EventQueue.java:97)
at java.awt.EventQueue$3.run(EventQueue.java:709)
at java.awt.EventQueue$3.run(EventQueue.java:703)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:80)
at java.awt.EventQueue.dispatchEvent(EventQueue.java:726)
at org.GNOME.Accessibility.AtkWrapper$5.dispatchEvent(AtkWrapper.java:700)
at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:201)
at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:116)
at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:105)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93)
at java.awt.EventDispatchThread.run(EventDispatchThread.java:82)

Похоже, что список words в методе paint модифицируется, пока над ним работает EDT. Я не изменяю его там, поэтому он должен быть из другого потока?
Я думал, что список был просто "моментальным снимком" списка words в другом потоке, так как он был отправлен методом publish. Видимо это не так. Так что же мне изменить, чтобы работать в методе EDT с таким "снимком" опубликованного списка из SwingWorker?

Спасибо за любой совет, который вы могли бы дать.

Примечания

  • После исключения программа продолжает работать нормально. Я только хотел бы сделать его чище.
  • Я попытался просмотреть Collections.synchronizedList() или даже synchronized (words){...}, но все мои попытки не увенчались успехом, скорее всего потому, что я понятия не имею, что в данном случае означает "синхронизм" и как его использовать.
  • Обратите внимание, что invokeLater в методе рисования поначалу казался бесполезным, потому что я всегда вызываю его из EDT, однако, если я его не использую, первый вызов paint не работает при создании графического интерфейса (это потому, что он выполняется до графический интерфейс, несмотря на то, что он вызывается после ?
  • Мне явно не хватает многих понятий, поэтому подробности будут действительно оценены.

person Ghislain Bugnicourt    schedule 01.08.2017    source источник
comment
Немного сложно предложить что-то конкретное, так как фрагменты кода фрагментированы, и нам приходится собирать их в своих головах. Если вы можете предоставить минимально воспроизводимый пример, это будет проще. Что касается вопроса: вы везде используете одну и ту же ссылку на список, поэтому вы получаете исключение. В качестве первого шага, чтобы создать снимок, просто вручную создайте новый список внутри doInBackground и передайте его publish. Посмотрите, поможет ли это.   -  person user1803551    schedule 01.08.2017
comment
Я знал, что с SSCCE лучше, однако я не чувствовал себя способным сделать это, потому что для воспроизведения проблемы требовалось, чтобы SwingWorker выполнял длительную задачу, во время которой он менял список, поэтому я думал, что не могу смоделировать это с паузой. В любом случае, к счастью, моя проблема заключалась в нескольких строках кода, которые я выбрал. :)   -  person Ghislain Bugnicourt    schedule 02.08.2017


Ответы (1)


Публикуя список words, созданный в SwingWorker, и продолжая работать с тем же экземпляром, вы подвергаете этот экземпляр синхронизации. Вы должны изменить метод doInBackground, чтобы создать новый List для публикации следующим образом:

public List<Word> doInBackground() {
  List<Word> words = new ArrayList<Word>();
  while (!isCancelled()) {
      // do some work, define aNewWord
      words.add(aNewWord);
      publish(new ArrayList<>(words)); // don't publish words directly but create new list
      Thread.pause(someTime);
  }
  return words;
}

С этим изменением фоновое задание и ваш метод рисования работают с отдельными объектами, и ваша проблема должна быть решена.

person dpr    schedule 01.08.2017
comment
Это работает спасибо! Я не могу поверить, что был так близок к ее решению: прежде чем спросить, я попытался создать копию методом рисования, но это не сработало. Это странно, потому что я только что попробовал, и это тоже решает проблему. Должно быть, я сделал это неправильно в первый раз. - person Ghislain Bugnicourt; 02.08.2017
comment
Создание нового списка в Paint не сработает, так как для создания нового списка вам также необходимо выполнить итерацию по старому. Это, вероятно, снизит вероятность возникновения проблемы, но время от времени вы будете получать исключение. На самом деле именно это делает параллелизм одним из самых сложных аспектов программирования. Просто выполняя свой код несколько раз, вы не можете сказать: я уверен, что он работает. Вы должны понимать различные пути, по которым могут пойти потоки, и всегда предполагать наихудший случай. - person dpr; 02.08.2017