ConcurrentModificationException при обновлении дерева JavaFX в фоновом режиме

Мой код создает TreeItem<String> в фоновой задаче, так как у меня их много и их создание занимает значительное количество времени, в течение которого приложение зависает. В этом примере это не имеет особого смысла, но иллюстрирует проблему, с которой я сталкиваюсь в своем реальном приложении. При развертывании узлов программа генерирует исключение ConcurrentModificationException.

Я использую jdk1.7.0_17 и JavaFX 2.2.7.

Кто-нибудь знает, как создать потокобезопасный Tree или как обойти проблему?

Исключение

java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:819)
    at java.util.ArrayList$Itr.next(ArrayList.java:791)
    at com.sun.javafx.collections.ObservableListWrapper$ObservableListIterator.next(ObservableListWrapper.java:681)
    at javafx.scene.control.TreeItem.updateExpandedDescendentCount(TreeItem.java:788)
    ...

Код

import javafx.application.Application;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

import java.security.SecureRandom;
import java.util.Random;


public class ConcurrentExample extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {
        TreeView<String> treeView = new TreeView<String>(createNode("root"));
        HBox hBox = new HBox();
        hBox.getChildren().addAll(treeView);
        Scene scene = new Scene(hBox);
        stage.setScene(scene);
        stage.show();
    }

    Random r = new SecureRandom();

    public TreeItem<String> createNode(final String b) {
        return new TreeItem<String>(b) {
            private boolean isLeaf;
            private boolean isFirstTimeChildren = true;
            private boolean isFirstTimeLeaf = true;

            @Override
            public ObservableList<TreeItem<String>> getChildren() {
                if (isFirstTimeChildren) {
                    isFirstTimeChildren = false;
                    buildChildren(super.getChildren());
                }
                return super.getChildren();
            }

            @Override
            public boolean isLeaf() {
                if (isFirstTimeLeaf) {
                    isFirstTimeLeaf = false;
                    isLeaf = r.nextBoolean() && r.nextBoolean() && r.nextBoolean();
                }
                return isLeaf;
            }

            private void buildChildren(final ObservableList<TreeItem<String>> children) {
                if (!this.isLeaf()) {
                    Task<Integer> task = new Task<Integer>() {
                        @Override
                        protected Integer call() throws Exception {
                            int i;
                            int max = r.nextInt(500);
                            for (i = 0; i <= max; i++) {
                                children.addAll(new TreeItem[]{createNode("#" + r.nextInt())});
                            }
                            return i;
                        }
                    };
                    new Thread(task).start();
                }
            }
        };
    }

}

person Sebastian Annies    schedule 20.04.2013    source источник


Ответы (3)


Текущие ответы не помогают. Дело в том, что вы должны выполнить обновление дочерних элементов в основном потоке с помощью Platform.runLater

import javafx.application.Application;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

import java.security.SecureRandom;
import java.util.Random;


public class Example extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {
        TreeView<String> treeView = new TreeView<String>(createNode("root"));
        HBox hBox = new HBox();
        hBox.getChildren().addAll(treeView);
        Scene scene = new Scene(hBox);
        stage.setScene(scene);
        stage.show();
    }

    Random r = new SecureRandom();

    public TreeItem<String> createNode(final String b) {
        return new TreeItem<String>(b) {
            private boolean isLeaf;
            private boolean isFirstTimeChildren = true;
            private boolean isFirstTimeLeaf = true;

            @Override
            public ObservableList<TreeItem<String>> getChildren() {
                if (isFirstTimeChildren) {
                    isFirstTimeChildren = false;
                    buildChildren(super.getChildren());
                }
                return super.getChildren();
            }

            @Override
            public boolean isLeaf() {
                if (isFirstTimeLeaf) {
                    isFirstTimeLeaf = false;
                    isLeaf = r.nextBoolean() && r.nextBoolean() && r.nextBoolean();
                }
                return isLeaf;
            }

            private void buildChildren(final ObservableList<TreeItem<String>> children) {
                if (!this.isLeaf()) {
                    Platform.runLater(new Runnable() {
                        @Override
                        public void run() {
                            int i;
                            int max = r.nextInt(500);
                            for (i = 0; i <= max; i++) {
                                children.addAll(new TreeItem[]{createNode("#" + r.nextInt())});
                            }
                        }
                    });
                }
            }
        };
    }
}

Этот парень столкнулся с той же проблемой: http://blog.idrsolutions.com/2012/12/handling-threads-concurrency-in-javafx/

person Sebastian Annies    schedule 30.06.2013
comment
В моем случае полезно знать, что Runnables выполняются в том порядке, в котором они были опубликованы. Runnable, переданный в метод runLater, будет выполнен до того, как какой-либо Runnable будет передан в последующий вызов запуститьПозже. Если этот метод вызывается после завершения работы среды выполнения JavaFX, вызов будет проигнорирован: Runnable не будет выполнен, и исключение не будет выдано. (irif.fr/~yunes /курс/приложения/java/docs/api/javafx/application/) - person Pixel; 14.01.2019

Вы не можете напрямую изменить что-либо, что влияет на активные узлы и данные, связанные с графом сцены (включая элементы TreeView), из любого потока, кроме потока приложения JavaFX.

Примеры задач (те, которые вернуть ObservableList или частичные результаты), которые помогут вам решить вашу проблему. Вам нужно создать новые TreeItems в своей задаче в новом ObservableList, а затем, как только задача будет завершена (и в потоке приложения JavaFX), установите список элементов для вашего дерева в ObservableList, возвращенный из задачи.

http://docs.oracle.com/javafx/2/api/javafx/concurrent/Task.html

Вот обновленная версия вашего кода, которая следует некоторым из этих принципов и не имеет исключений ConcurrentModificationException.

Почему вызов addAll(List) не должен выполняться именно в тот момент, когда TreeItem вызывает updateExpandedDescidentCount()?

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

import java.security.SecureRandom;
import java.util.Random;


public class ConcurrentExample extends Application {
  public static void main(String[] args) {
    launch(args);
  }

  @Override
  public void start(Stage stage) throws Exception {
    TreeView<String> treeView = new TreeView<>(createNode("root"));
    HBox hBox = new HBox();
    hBox.getChildren().addAll(treeView);
    Scene scene = new Scene(hBox);
    stage.setScene(scene);
    stage.show();
  }

  Random r = new SecureRandom();

  public TreeItem<String> createNode(final String b) {
    return new TreeItem<String>(b) {
      private boolean isLeaf;
      private boolean isFirstTimeChildren = true;
      private boolean isFirstTimeLeaf = true;

      @Override
      public ObservableList<TreeItem<String>> getChildren() {
        if (isFirstTimeChildren) {
          isFirstTimeChildren = false;
          buildChildren(super.getChildren());
        }
        return super.getChildren();
      }

      @Override
      public boolean isLeaf() {
        if (isFirstTimeLeaf) {
          isFirstTimeLeaf = false;
          isLeaf = r.nextBoolean() && r.nextBoolean() && r.nextBoolean();
        }
        return isLeaf;
      }

      private void buildChildren(final ObservableList<TreeItem<String>> children) {
        final ObservableList<TreeItem<String>> taskChildren = FXCollections.observableArrayList();

        if (!this.isLeaf()) {
          Task<Integer> task = new Task<Integer>() {
            @Override
            protected Integer call() throws Exception {
              int i;
              int max = r.nextInt(500);
              for (i = 0; i <= max; i++) {
                taskChildren.addAll(new TreeItem[]{createNode("#" + r.nextInt())});
              }
              return i;
            }
          };

          task.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
            @Override public void handle(WorkerStateEvent workerStateEvent) {
              children.setAll(taskChildren);
            }
          });
          new Thread(task).start();
        }
      }
    };
  }

}

Обновление — описание того, почему решение работает

Приведенное выше решение не может получить ConcurrentModificationException, потому что задействованные ObservableLists никогда не изменяются одновременно.

  • Коллекции taskChildren изменяются только в пользовательском потоке для задачи И
  • Дочерние элементы дерева, активно прикрепленные к графу сцены, изменяются только в потоке приложения JavaFX для задачи.

Это обеспечивается следующими пунктами:

  1. taskChildren.addAll вызывается в методе call задачи.
  2. Метод вызова задачи вызывается в пользовательском потоке.
  3. children.setAll(taskChildren) вызывается в потоке приложения JavaFX.
  4. Система JavaFX гарантирует, что обработчик событий onSucceeded для задачи вызывается в потоке приложения JavaFX.
  5. После завершения задачи в заданный список taskChildren больше не будут добавляться дочерние элементы, и этот список никогда не будет изменен.
  6. Для каждой выполненной задачи создается новый список taskChildren, поэтому заданный список taskChildren никогда не используется совместно задачами.
  7. Каждый раз, когда в дерево вносятся изменения, создается новая задача.
  8. Семантика задачи такова, что данная задача может быть запущена только один раз и никогда не перезапускаться.
  9. Дочерние элементы TreeItem, прикрепленные к активному графу сцены, изменяются только в потоке приложения JavaFX после успешного завершения задачи и прекращения обработки.

Почему вызов addAll(List) не должен выполняться именно в тот момент, когда TreeItem вызывает updateExpandedDescendentCount()?

updateExpandedDescendentCount() не является частью общедоступного API TreeItem — это метод внутренней реализации TreeView, который не имеет отношения к решению этой проблемы.


Обновить частичные обновления

В документации по задачам JavaFX есть решение для "Задачи Что возвращает частичные результаты». Используя что-то подобное, вы сможете решить проблему, заключающуюся в том, что «приложение непригодно для использования в начале, так как нужно дождаться завершения потока 'buildChildren', чтобы увидеть какие-либо узлы». Это связано с тем, что решение с частичным результатом позволит «передавать» результаты небольшими партиями из потока задачи компоновщика обратно в поток приложения FX.

Такое решение сложнее в реализации, чем то, которое я представил выше, но оно должно позволить вам иметь отзывчивый пользовательский интерфейс, соответствующий вашим требованиям. Как всегда, при работе с одновременными ситуациями необходимо проявлять особую осторожность, чтобы гарантировать, что общие данные не изменятся одновременно, вызывая потенциальные условия гонки, как вы испытали в своем исходном сообщении.

person jewelsea    schedule 22.04.2013
comment
Я добавил описание того, почему прилагаемое решение работает по замыслу, а не по счастливой случайности. - person jewelsea; 22.04.2013
comment
@jewelsead: Пункт 5 - важный момент. Мне кажется, что updateExpandedDescendentCount() вызывается в какой-то момент времени после того, как TreeItem было добавлено логикой TreeView. Когда новый узел добавляется во время этих операций «после добавления элемента», мы получаем ConcurrentModificationException. Таким образом, addAll() с TreeItems в одной операции никогда не вызовет исключения. --- Это решает проблему зависания приложения. Но, тем не менее, вначале приложение непригодно для использования, так как нужно дождаться завершения потока 'buildChildren', чтобы увидеть какие-либо узлы. - person Sebastian Annies; 23.04.2013
comment
Добавлены некоторые дополнительные предложения о том, как вы можете использовать частичные обновления для решения проблемы: приложение вначале непригодно для использования, так как нужно дождаться завершения потока 'buildChildren', чтобы увидеть какие-либо узлы. - person jewelsea; 24.04.2013
comment
Твой ответ был настоящей находкой, @jewelsea. Помог мне ответить на мою проблему здесь: stackoverflow.com/questions/22072114/ - person Dimitris Sfounis; 27.02.2014

Это просто: вы продолжаете обновлять коллекцию, когда клиентский код начинает ее перебирать.

Удалите поток или убедитесь, что он завершен до создания итератора, или выполните какую-либо синхронизацию.

person Valeri Atamaniouk    schedule 20.04.2013
comment
Я знаю, что происходит. Но: мне необходимо обновить Tree в фоновом режиме, так как мое фактическое приложение может иметь около 20000 TreeItem на верхнем уровне, и для их создания требуется значительное количество времени. В это время приложение не отвечает. Таким образом, естественный способ обойти это — создать фоновый поток для создания узлов. Какая-то синхронизация... конечно, но как? Извините, но ни одно из ваших предложений мне не подходит. - person Sebastian Annies; 21.04.2013