JavaFX TableView高频率更新

时间:2016-04-13 09:06:23

标签: multithreading javafx tableview updates

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

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

但后来我发现,如果我向属性添加一个空的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);
  }
}

1 个答案:

答案 0 :(得分:2)

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

此外,您的代码最终会有太多请求来更新UI。因此,即使你修复了线程问题,你也需要以某种方式限制请求,这样你就不会淹没FX应用程序线程有太多的更新请求,这会使它无响应。

Throttling javafx gui updates解决了执行此操作的技巧。我将在表模型类的上下文中重复它:

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来“遮蔽”不动产。以原子方式更新它并检查它是否为null,如果是,则安排更新FX应用程序线程上的不动产。在更新中,以原子方式检索“阴影”值并将其重置为null,并将real属性设置为检索到的值。这样可以确保FX应用程序线程上的新请求仅在FX应用程序线程消耗它们时进行,确保FX应用程序线程不会被淹没。当然,如果在FX应用程序线程上安排更新和实际发生更新之间存在延迟,那么当更新确实发生时,它仍将检索设置了“阴影”值的最新值。

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

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