java 在后台更新 JavaFX 树时出现 ConcurrentModificationException

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/16125311/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-31 21:59:05  来源:igfitidea点击:

ConcurrentModificationException when updating JavaFX Tree in the background

javajavafx-2

提问by Sebastian Annies

My code creates TreeItem<String>in a background task since I have a lot of them and their creation takes a considerable amount of time in which the application freezes. In this example it doesn't make much sense but it illustrates the problem I run into in my actual application. When expanding nodes the program throws a ConcurrentModificationException.

我的代码TreeItem<String>在后台任务中创建,因为我有很多它们,并且它们的创建需要相当长的时间,应用程序在其中冻结。在这个例子中,它没有多大意义,但它说明了我在实际应用程序中遇到的问题。扩展节点时,程序会抛出 ConcurrentModificationException。

I use jdk1.7.0_17 and JavaFX 2.2.7

我使用 jdk1.7.0_17 和 JavaFX 2.2.7

Does anyone know how to create a thread-safe Treeor how to circumvent the problem?

有谁知道如何创建线程安全Tree或如何规避问题?

Exception

例外

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)
    ...

Code

代码

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();
                }
            }
        };
    }

}

回答by Sebastian Annies

The current answers do not help. The point is that you have to execute the update of the children in the main Thread with the help of Platform.runLater

目前的答案没有帮助。关键是你必须在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())});
                            }
                        }
                    });
                }
            }
        };
    }
}

This guy here ran into the same problem: http://blog.idrsolutions.com/2012/12/handling-threads-concurrency-in-javafx/

这个家伙在这里遇到了同样的问题:http: //blog.idrsolutions.com/2012/12/handling-threads-concurrency-in-javafx/

回答by jewelsea

You can't directly modify anything that effects active nodes and data related to the scene graph (and that includes the items of a TreeView) from any thread other than the JavaFX application thread.

您不能从 JavaFX 应用程序线程以外的任何线程直接修改影响活动节点和与场景图(包括 TreeView 的项目)相关的数据的任何内容。

See the Taskdocumentation for sample tasks (the ones which return an ObservableList or Partial Results) which will help you solve your issue. You need to create your new TreeItems in your Task in a new ObservableList and then, once the Task is finished (and on the JavaFX application thread), set the item list for your tree to the ObservableList returned from the Task.

请参阅任务文档了解示例任务(返回 ObservableList 或部分结果的任务),这将帮助您解决问题。您需要在新的 ObservableList 中的任务中创建新的 TreeItems,然后,一旦任务完成(并在 JavaFX 应用程序线程上),将树的项目列表设置为从任务返回的 ObservableList。

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

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

Here is an updated version of your code which follows some of these principles and does not have any ConcurrentModificationExceptions.

这是您的代码的更新版本,它遵循其中一些原则并且没有任何 ConcurrentModificationExceptions。

Why shouldn't the addAll(List) call be performed exactly in the moment where the TreeItem calls updateExpandedDescendentCount()?

为什么不应该在 TreeItem 调用 updateExpandedDescendentCount() 的那一刻执行 addAll(List) 调用?

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();
        }
      }
    };
  }

}


Update - Description of why the solution works

更新 - 解决方案为何有效的说明

The solution above cannot receive a ConcurrentModificationExceptionbecause the ObservableListsinvolved are never modified concurrently.

上面的解决方案无法接收 a,ConcurrentModificationException因为ObservableLists涉及的内容永远不会被并发修改。

  • The taskChildrencollections are only modified on the user thread for the task AND
  • Children of tree items actively attached to the scenegraph are only modified on the JavaFX application thread for the task.
  • taskChildren集合只在用户线程修改任务,
  • 主动附加到场景图形的树项的子项仅在任务的 JavaFX 应用程序线程上进行修改。

This is ensured by the following items:

这是由以下项目确保的:

  1. taskChildren.addAllis invoked in the task's callmethod.
  2. A task's call method is invoked on the user thread.
  3. children.setAll(taskChildren)is invoked on the JavaFX application thread.
  4. The JavaFX system ensures that the onSucceededevent handler for a task is invoked on the JavaFX application thread.
  5. After task completion, no more children will ever be added to a given taskChildrenlist and the list is never modified.
  6. For every task undertaken a new taskChildrenlist is created, so a given taskChildrenlist is never shared between tasks.
  7. Every time a modification is made to the tree, a new task is created.
  8. Task semantics are such that a given task can only be run once and never restarted.
  9. The children of the TreeItem attached to the active scenegraph are only modified on the JavaFX application thread after the task has successfully completed and stopped processing.
  1. taskChildren.addAll在任务的call方法中调用。
  2. 在用户线程上调用任务的调用方法。
  3. children.setAll(taskChildren)在 JavaFX 应用程序线程上调用。
  4. JavaFX 系统确保onSucceeded在 JavaFX 应用程序线程上调用任务的事件处理程序。
  5. 任务完成后,将不会再将更多子项添加到给定taskChildren列表中,并且永远不会修改该列表。
  6. 对于执行的每个任务,taskChildren都会创建一个新列表,因此taskChildren在任务之间永远不会共享给定的列表。
  7. 每次对树进行修改时,都会创建一个新任务。
  8. 任务语义使得给定的任务只能运行一次并且永远不会重新启动。
  9. 附加到活动场景图的 TreeItem 的子项仅在任务成功完成并停止处理后在 JavaFX 应用程序线程上进行修改。

Why shouldn't the addAll(List)call be performed exactly in the moment where the TreeItemcalls updateExpandedDescendentCount()?

为什么不应该在addAll(List)呼叫的那一刻准确地执行TreeItem呼叫updateExpandedDescendentCount()

updateExpandedDescendentCount()is not part of the public TreeItemapi - it is an internal implementation method for the TreeViewand is irrelevant to solving this problem.

updateExpandedDescendentCount()不是公共TreeItemapi 的一部分- 它是 的内部实现方法,TreeView与解决此问题无关。



Update Partial Updates

更新部分更新

The JavaFX Task documentationhas a solution for "A Task Which Returns Partial Results". Using something similar, you should be able to solve the issue that "the application is unusable in the beginning as one has to wait for the 'buildChildren'-thread to finish to see any nodes.". This is because the partial result solution will allow the results to be "streamed" in small batches from the builder task thread back to the FX application thread.

JavaFX的工作文件中是否有“任务返回的部分结果”的解决方案。使用类似的东西,您应该能够解决“应用程序一开始无法使用,因为必须等待‘buildChildren’线程完成才能看到任何节点的问题。”。这是因为部分结果解决方案将允许结果以小批量从构建器任务线程“流”回 FX 应用程序线程。

This kind of solution is more complicated in implementation than the one I provided above, but should allow you to have a responsive UI that fits your requirements. As always, when dealing with concurrent situations, extra care will need to be taken to ensure that shared data is not mutated concurrently causing potential race conditions as you experienced in your original post.

这种解决方案的实现比我上面提供的解决方案更复杂,但应该允许您拥有符合您要求的响应式 UI。与往常一样,在处理并发情况时,需要格外小心,以确保共享数据不会同时发生变异,从而导致您在原始帖子中遇到的潜在竞争条件。

回答by Valeri Atamaniouk

It is straightforward: you continue to update collection when the client code starts iterating it.

很简单:当客户端代码开始迭代它时,您继续更新集合。

Remove the thread or make sure it is finished before iterator is created, or make some kind of synchronization.

在创建迭代器之前删除线程或确保它已完成,或进行某种同步。