读取Javafx控件的属性是线程安全的吗



我知道在Javafx线程之外写入Javafx控件属性是不安全的。但是阅读也不安全吗?

这就是我想做的:

void runNoneFXThreadCode() {
// Calculate some slow math operation
...
// Now get the current value of Javafx textfield
String result = textfield.getText()

// Calculate another slow math operation based on result
...
}

以线程安全的方式实现上述目标的最佳方法是什么?

我一直在没有runLater的情况下做这件事,而且在Windows中从未遇到过问题。但我想知道它在Mac或Linux中是否有问题。

如果这个阅读不是线程安全的,请留下明确提到阅读不安全的规范的链接。我能找到的大多数人都只谈论写作不安全。

线程安全

从根本上讲,当涉及到Java中的线程安全时,唯一重要的是在创建关系之前是否发生了必要的。其中一些规则可能非常微妙(例如围绕volatile的规则)。要了解更多信息,请查看:

  • java.util.concurrent软件包文档
  • §17.4.5发生在Java语言规范的顺序之前

JavaFX

JavaFX显然不是线程安全的。

JavaFX场景图表示JavaFX应用程序的图形用户界面,它不是线程安全的,只能访问[emphasis added]并从UI线程(也称为JavaFX应用线程)进行修改。

通常,JavaFX库的对象并不是为了同时处理在其上操作的并发线程而实现的。无论涉及哪些线程,这都是正确的。更狭义地说,一旦对象连接到";活的";UI,其中";活的";本质上是指";显示";,则必须仅在JavaFX应用程序线程上访问和修改这些对象。在各种文档中多次提到此限制。并且一些对象,例如WindowWebView,具有更严格的限制,其中它们必须决不能被除FX线程之外的线程触摸,而不管它们是否是";生活";。

注意JavaFX中的一些操作将首先检查它们是否在JavaFX应用程序线程上调用,如果没有,则抛出IllegalStateException。但是许多操作将而不是执行此检查。


解决您的问题

你问:

以线程安全的方式实现上述目标的最佳方法是什么?

如果您只需要在后台任务中间读取一个TextField,那么一个快速且有点脏的解决方案是使用CompletableFuture。替换:

String result = textField.getText();

带有:

String result =
CompletableFuture
.supplyAsync(textField::getText, Platform::runLater)
.join();

但你似乎也有两个独立的任务,第二个任务取决于第一个任务的结果。在这种情况下,使用Task的两个实现,并在第一个实现成功时启动第二个实现。@James_D的回答证明了这种方法。另请参阅JavaFX中的并发。


在后台线程上读取UI状态安全吗

答案基本上是;不,但这也有点取决于"。

简短回答

不,在后台线程上读取UI状态是不安全的。只要表现得像这总是真的,如果你能出错,我会感到惊讶。然而,试图推断一个特定场景是否是线程安全的,很容易在未来导致错误或中断。

答案很长

在最简单的情况下,在后台线程上读取UI状态不太可能导致任何不希望的副作用。例如,以下应用程序:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main extends Application {

@Override
public void start(Stage primaryStage) {
var label = new Label("Hello, World!");

primaryStage.setScene(new Scene(new StackPane(label), 500, 300));
primaryStage.setTitle("Thread-Safety");
primaryStage.show();
var thread = new Thread(() -> System.out.println(label.getText()));
thread.setDaemon(true);
thread.start();
}
}

如果我没有弄错的话,应该是线程安全的。标签的文本属性是在启动后台线程之前设置的。也就是说,设置文本属性发生在启动后台线程之前。因此,后台线程应该始终读取最新的值(即,不读取过时的值)。

现在,人们可能会认为这种事情总是安全的,因为你只是在读状态,而不是在写状态。但是,您不能总是确定调用一个方法肯定不会导致任何写入。例如,标签的文本属性继承自StringPropertyBase(实现细节)。并且该类保持一个";有效的";状态当您调用getText()时,最终会导致执行valid = true。警觉的这是一篇文章!这可能会引起问题吗?在这种特殊情况下可能不会。但问题是,你并不总是知道像阅读某些状态这样无害的东西会引起什么副作用(除非文档另有解释)。

即使没有写入,您也可能最终读取到一个过时的值。假设您的用户试图删除一个目录,但他们选择了错误的目录。他们在开始删除操作之前修复了它,但不知何故,您的任务读取了旧的不正确的目录,并删除了该目录。考虑到UI交互的性质,我的直觉认为这种情况即使不是不可能,也不太可能发生,但想象一下你的用户不可恢复地删除他们的家庭照片而不是一些旧文档时的恐惧。当他们意识到这是由应用程序中的错误引起的时,他们会感到愤怒。最好只对应用程序进行编码,这样除非用户出错,否则不可能发生这种情况。

总之,您应该假设从后台线程读取UI状态是不安全的这在一般Java编程中也是一种很好的做法。假设一个对象是而不是线程安全的,除非另有明确记录;在线程之间通信时,始终确保在创建关系之前发生

此外,编写以下100%线程安全的代码并不困难:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main extends Application {

@Override
public void start(Stage primaryStage) {
var label = new Label("Hello, World!");

primaryStage.setScene(new Scene(new StackPane(label), 500, 300));
primaryStage.setTitle("Thread-Safety");
primaryStage.show();
// Read UI state on FX thread, pass values to background task
var text = label.getText();
var thread = new Thread(() -> System.out.println(text));
thread.setDaemon(true);
thread.start();
}
}

在您发布的代码中,基本上有两个操作需要在后台运行。为每个操作创建单独的任务。所以这看起来像:

private Executor exec = Executors.newCachedThreadPool();
void runNoneFXCode() {
Task<SomeType> firstTask = new Task<>() {
@Override
protected SomeType call() {
return doFirstOperation();
}
};
firstTask.setOnSucceeded(event -> {
String text = textField.getText();
SomeType computationResult = firstTask.getValue();
Task<SomeOtherType> secondTask = new Task<>() {
@Override
protected SomeOtherType call() {
return doSecondOperation(text, computationResult);
}
};
secondTask.setOnSucceeded(event -> {
SomeOtherType completeResult = secondTask.getValue();
// update UI with completeResult
});
exec.execute(secondTask);
});
exec.execute(firstTask);
}
private SomeType doFirstOperation() {
// long computation...
return result ;
}
private SomeOtherType doSecondOperation(String text, SomeType result) {
// long computation...
return result ;
}

最新更新