在后台更新JavaFX树时出现ConcurrentModificationException

时间:2013-04-20 21:18:50

标签: java javafx-2

我的代码在后台任务中创建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();
                }
            }
        };
    }

}

3 个答案:

答案 0 :(得分:8)

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

答案 1 :(得分:3)

您无法直接修改任何影响活动节点的任何内容以及与JavaFX应用程序线程以外的任何线程相关的场景图(以及包含TreeView项)的数据。

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

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

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

为什么不应该在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();
        }
      }
    };
  }

}

更新 - 解决方案工作原理的说明

上述解决方案无法收到ConcurrentModificationException,因为所涉及的ObservableLists永远不会同时修改。

  • 仅在任务AND的用户线程上修改taskChildren个集合
  • 主动附加到场景图的树项目的子项仅在任务的JavaFX应用程序线程上进行修改。

以下项目确保了这一点:

  1. taskChildren.addAll在任务的call方法中调用。
  2. 在用户线程上调用任务的调用方法。
  3. 在JavaFX应用程序线程上调用
  4. children.setAll(taskChildren)
  5. JavaFX系统确保在JavaFX应用程序线程上调用任务的onSucceeded事件处理程序。
  6. 任务完成后,不会再将子项添加到给定的taskChildren列表中,并且永远不会修改该列表。
  7. 对于所执行的每项任务,都会创建一个新的taskChildren列表,因此任务之间永远不会共享给定的taskChildren列表。
  8. 每次对树进行修改时,都会创建一个新任务。
  9. 任务语义使得给定任务只能运行一次并且永远不会重新启动。
  10. 只有在任务成功完成并停止处理后,才会在JavaFX应用程序线程上修改附加到活动场景图的TreeItem的子项。
  11.   

    为什么不应在addAll(List)调用TreeItem的时刻准确执行updateExpandedDescendentCount()来电?

    updateExpandedDescendentCount()不属于公开TreeItem API - 它是TreeView的内部实施方法,与解决此问题无关。


    更新部分更新

    JavaFX Task documentation有一个“返回部分结果的任务”的解决方案。使用类似的东西,您应该能够解决“应用程序在开始时无法使用的问题,因为必须等待'buildChildren'线程完成才能看到任何节点。”这是因为部分结果解决方案将允许结果从构建器任务线程小批量“流式传输”回FX应用程序线程。

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

答案 2 :(得分:0)

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

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