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