清除具有大量形状/多线程的组的最快方法

Enr*_*ico 8 java concurrency multithreading javafx javafx-11

在我的 JavaFX 项目中,我使用大量形状(例如 1 000 000)来表示地理数据(例如地块轮廓、街道等)。它们存储在一个组中,有时我必须清除它们(例如,当我加载包含新地理数据的新文件时)。问题是:清除/删除它们需要很多时间。所以我的想法是在单独的线程中删除形状,这显然由于 JavaFX 单线程而不起作用。

这是我想要做的事情的简化代码:

HelloApplication.java

package com.example.javafxmultithreading;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.shape.Line;
import javafx.stage.Stage;

import java.io.IOException;

public class HelloApplication extends Application {

    public static Group group = new Group();

    @Override
    public void start(Stage stage) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
        Scene scene = new Scene(fxmlLoader.load());
        stage.setTitle("Hello!");
        stage.setScene(scene);
        stage.show();

        for (int i = 0; i < 1000000; i++) {
            group.getChildren().add(new Line(100, 200, 200, 300));
        }
        HelloController.helloController = fxmlLoader.getController();
        HelloController.helloController.pane.getChildren().addAll(group);
    }

    public static void main(String[] args) {
        launch();
    }
}
Run Code Online (Sandbox Code Playgroud)

HelloController.java

public class HelloController {

    public static HelloController helloController;
    @FXML
    public Pane pane;
    public VBox vbox;

    @FXML
    public void onClearShapes() throws InterruptedException {
        double start = System.currentTimeMillis();
        HelloApplication.group.getChildren().clear();
        System.out.println(System.currentTimeMillis() - start);

        Service<Boolean> service = new Service<>() {
            @Override
            protected Task<Boolean> createTask() {
                return new Task<>() {
                    @Override
                    protected Boolean call() {
                        // Try to clear the children of the group in this thread
                        return true;
                    }
                };
            }
        };
        service.setOnSucceeded(event -> {
            System.out.println("Success");
        });
        service.start();
    }
}
Run Code Online (Sandbox Code Playgroud)

你好视图.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<VBox fx:id="vbox" alignment="CENTER" prefHeight="465.0" prefWidth="711.0" spacing="20.0"
      xmlns="http://javafx.com/javafx/11.0.2" xmlns:fx="http://javafx.com/fxml/1"
      fx:controller="com.example.javafxmultithreading.HelloController">
    <padding>
        <Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
    </padding>
    <Pane fx:id="pane" prefHeight="200.0" prefWidth="200.0"/>
    <Button mnemonicParsing="false" onAction="#onClearShapes" text="Clear shapes"/>
</VBox>
Run Code Online (Sandbox Code Playgroud)

我测量了从小组中移除不同数量的孩子所需的时间group.getChildren().clear()

amount of children    |   time
100                       2ms = 0,002s
1 000                     4ms = 0,004s
10 000                    38ms = 0,038s
100 000                   1273ms = 1,2s
1 000 000                 149896ms = 149,896s = ~2,5min
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,所需的时间呈指数级增长。现在想象一下,您必须清除 UI 中的子级,并且用户必须在应用程序冻结时等待 2.5 分钟。此外,在这个简化的示例中,它只是一条简单的线,在“真实”应用程序中,它是一个更复杂的几何形状 -> 需要更多时间。

因此,另一个想法是将该组与其父窗格“解除绑定”。因为当它解除绑定时,我可以在另一个线程中将其删除。这意味着 1. 用户界面不会冻结,2. 速度会更快。这是尝试:

pane.getChildren().remove(group); // or clear()
// and then clear the group in another thread like above
Run Code Online (Sandbox Code Playgroud)

问题是:这种“解除绑定”也需要很多时间。不是2.5分钟,而是0.5分钟,还是太多了。

另一个想法是创建多个组,因为正如您所看到的,具有 10 000 或 100 000 个元素的组清除速度更快。这也失败了,因为几个组突然花费了更长的时间并且删除速度呈指数级增长。例如,第一个需要 20 秒,第二个需要 10 秒,第三个需要 5 秒,等等。

长话短说

是否有机会在单独的线程中删除该组的子级,或者比使用更快group.getChildren().clear()?我尝试了我想到的一切......

如果我在删除时只能显示加载栏,那会比仅仅冻结表面并等待 2 分钟要好......

我感谢每一个想法/帮助。

编辑,请参阅注释 没有 FXML 的简单示例:

import javafx.scene.Group;
import javafx.scene.shape.Line;

public class Test {

    public static void main(String[] args) {
        Group group = new Group();
        System.out.println("adding lines");
        for (int i = 0; i < 1000000; i++) {
            group.getChildren().add(new Line(100, 200, 200, 300));
        }
        System.out.println("adding done");

        System.out.println("removing starts");
        double start = System.currentTimeMillis();
        group.getChildren().clear();
        System.out.println("removing done, needed time: " + (System.currentTimeMillis() - start));
    }
}
Run Code Online (Sandbox Code Playgroud)

Sla*_*law 10

尚未对其进行测试,但这种情况下执行时间较长的根本原因可能已在 JavaFX 21+ 中使用JDK-8290765 - 删除父禁用/treeVisible 侦听器“修复” 。也就是说,我仍然认为拥有数百万个节点本身就是一个问题。节点太多了。


执行时间较长是因为 a 的每个子级都使用该 的和属性Parent注册了一个侦听器。JavaFX 目前的实现方式是,这些监听器存储在一个数组(即列表结构)中。添加侦听器的成本相对较低,因为新侦听器只是简单地插入到数组的末尾,偶尔会调整数组的大小。但是,当您从其中删除子级并删除侦听器时,需要线性搜索数组,以便找到并删除正确的侦听器。每个被移除的孩子都会发生这种情况。disabledtreeVisibleParentParent

因此,当您清除 的子列表时,Group您将触发这两个属性的 1,000,000 次线性搜索,总共导致 2,000,000 次线性搜索。更糟糕的是,要删除的侦听器要么(取决于删除子级的顺序)始终位于数组的末尾(在这种情况下,有 2,000,000 个最坏情况线性搜索),要么始终位于数组的开头数组,在这种情况下,有 2,000,000 次最佳情况线性搜索,但每次删除都会导致所有剩余元素必须移动一位。

至少有两种解决方案/解决方法:

  1. 不显示 1,000,000 个节点。如果可以的话,尝试只显示用户实际可以看到的数据的节点。例如,虚拟化控件在任何给定时间ListViewTableView显示大约1-100个单元格(取决于各种因素,包括窗口大小、屏幕大小等)。

  2. 不要清除 的子级Group。相反,只需Group用新的替换旧的GroupGroup如果需要,您可以在后台线程中准备新的。

    这样做,在我的计算机上花了 3.5 秒创建了另一个Group包含 1,000,000 个子级的文件,然后Group用新的替换了旧的Group. 然而,由于需要立即渲染所有新节点,仍然存在一点滞后峰值。

    如果您不需要填充新的,Group那么您甚至不需要线程。在这种情况下,我的计算机上的交换大约花费了 0.27 秒。

  • 关于您最初的问题,我仍然不知道为什么您认为必须将那么多单独的节点添加到场景图中。我也从事地理业务,但我只是没有看到有效的用例。您是否考虑过路径或其他表示图形元素的方式?你能举个例子来说明你真正想画什么吗? (2认同)