JavaFX TableView 更新频率高

JavaFX TableView high frequent updates

我尝试以高频率更新 JFX TableView(概念验证应用程序)中的单元格。我通过 FXML 加载 TableView 并启动 ExecutorService 来更改单元格的值。

当我启动应用程序时,我注意到,更新适用于前 3-4 百万个元素,然后就卡住了。如果我放慢更新速度(请参阅 MAGIC#1),它会起作用(10 毫秒仍然太快,但 100 毫秒延迟会起作用)。所以我认为这可能是线程问题。

但后来我发现,如果我向 属性 添加一个空的 ChangeListener(请参阅 MAGIC#2),它就可以正常工作。即使不需要 MAGIC#1.

我是不是做错了什么?我必须以不同的方式更新单元格吗?

在此先感谢您的帮助!!

TableView中的元素:

public class Element {
  public static final AtomicInteger x = new AtomicInteger(0);
  private final StringProperty nameProperty = new SimpleStringProperty("INIT");

  public Element() {
    // MAGIC#2
    // this.nameProperty.addListener((observable, oldValue, newValue) -> {});
  }

  public void tick() {
    this.setName(String.valueOf(x.incrementAndGet()));
  }

  public String getName() ...
  public void setName(String name)...
  public StringProperty nameProperty() ...
}

FXML 控制器:

public class TablePerformanceController implements Initializable {
  private final ObservableList<Element> data = FXCollections.observableArrayList();

  public Runnable changeValues = () -> {
    while (true) {
      if (Thread.currentThread().isInterrupted()) break;
      data.get(0).tick();
      // MAGIC#1
      // try { Thread.sleep(100); } catch (Exception e) {}
    }
  };

  private ExecutorService executor = null;

  @FXML
  public TableView<Element> table;

  @Override
  public void initialize(URL location, ResourceBundle resources) {
    this.table.setEditable(true);

    TableColumn<Element, String> nameCol = new TableColumn<>("Name");
    nameCol.setCellValueFactory(cell -> cell.getValue().nameProperty());
    this.table.getColumns().addAll(nameCol);

    this.data.add(new Element());
    this.table.setItems(this.data);

    this.executor = Executors.newSingleThreadExecutor();
    this.executor.submit(this.changeValues);
  }
}

您违反了 JavaFX 的单线程规则:对 UI 的更新只能从 FX 应用程序线程进行。您的 tick() 方法更新了 nameProperty(),并且由于 table 单元正在观察 nameProperty()tick() 导致对 UI 的更新。由于您是从后台线程调用 tick(),因此对 UI 的更新发生在后台线程上。由此产生的行为本质上是未定义的。

此外,您的代码最终会收到太多更新 UI 的请求。因此,即使您修复了线程问题,您也需要以某种方式限制请求,以免 FX 应用程序线程充满太多要更新的请求,这会导致它无响应。

Throttling javafx gui updates 中介绍了执行此操作的技术。我将在 table 模型 class:

的上下文中重复
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Element {

    // Note that in the example we only actually reference this from a single background thread,
    // in which case we could just make this a regular int. However, for general use this might
    // need to be threadsafe.
    private final AtomicInteger x = new AtomicInteger(0);

    private final StringProperty nameProperty = new SimpleStringProperty("INIT");

    private final AtomicReference<String> name = new AtomicReference<>();


    /** This method is safe to call from any thread. */
    public void tick() {
        if (name.getAndSet(Integer.toString(x.incrementAndGet())) == null) {
            Platform.runLater(() -> nameProperty.set(name.getAndSet(null)));
        }
    }

    public String getName() {
        return nameProperty().get();
    }

    public void setName(String name) {
        nameProperty().set(name);
    }

    public StringProperty nameProperty() {
        return nameProperty;
    }
}

这里的基本思路是用一个AtomicReference<String到"shadow"真正的属性。自动更新它并检查它是否为 null,如果是,则在 FX 应用程序线程上安排对真实 属性 的更新。在更新中,自动检索 "shadow" 值并将其重置为 null,并将真正的 属性 设置为检索到的值。这确保了在 FX 应用程序线程上更新的新请求仅在 FX 应用程序线程使用它们时发出,确保 FX 应用程序线程不会被淹没。当然,如果在 FX 应用程序线程上安排更新与实际发生更新之间存在延迟,则当更新确实发生时,它仍将检索设置了 "shadow" 值的最新值。

这是一个独立的测试,基本上等同于您展示的控制器代码:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;

public class FastTableUpdate extends Application {

    private final ObservableList<Element> data = FXCollections.observableArrayList();

    public final Runnable changeValues = () -> {
      while (true) {
        if (Thread.currentThread().isInterrupted()) break;
        data.get(0).tick();
      }
    };

    private final ExecutorService executor = Executors.newSingleThreadExecutor(runnable -> {
        Thread t = new Thread(runnable);
        t.setDaemon(true);
        return t ;
    });



    @Override
    public void start(Stage primaryStage) {

        TableView<Element> table = new TableView<>();
        table.setEditable(true);

        TableColumn<Element, String> nameCol = new TableColumn<>("Name");
        nameCol.setPrefWidth(200);
        nameCol.setCellValueFactory(cell -> cell.getValue().nameProperty());
        table.getColumns().add(nameCol);

        this.data.add(new Element());
        table.setItems(this.data);

        this.executor.submit(this.changeValues);        

        Scene scene = new Scene(table, 600, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}