我尝试高频率更新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);
}
}
答案 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);
}
}