Issue
I know it's unsafe to write to Javafx control properties outside of Javafx thread. But is it also unsafe to read?
This is what I want to do:
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
...
}
What's the best way to achieve above in thread safe manner?
I've been doing it without runLater
and never had issues in Windows. But I wonder if it might be problematic in Mac or Linux.
If this reading is not thread-safe please leave the link to specification that explicitly mentions reading is unsafe. Most I could find only talk about writing being unsafe.
Solution
Thread-Safety
Fundamentally, the only thing that matters when it comes to thread-safety in Java is whether or not the necessary happens-before relationships have been created. Some of the rules can be quite subtle (such as those surrounding volatile
). To learn more about this, check out:
- The
java.util.concurrent
package documentation. - §17.4.5 Happens-before Order of the Java Language Specification.
JavaFX
JavaFX is explicitly not thread-safe.
The JavaFX scene graph, which represents the graphical user interface of a JavaFX application, is not thread-safe and can only be accessed [emphasis added] and modified from the UI thread also known as the JavaFX Application thread.
In general, the objects of the JavaFX libraries are not implemented to handle concurrent threads operating on them simultaneously. This is true regardless of which threads are involved. More narrowly, once an object is connected to the "live" UI, where "live" essentially means "part of a window that is showing", then those objects must only be accessed and modified on the JavaFX Application Thread. This restriction is mentioned many times throughout the various documentation. And some objects, such as Window
and WebView
, have even stricter restrictions where they must never be touched by a thread other than the FX thread, regardless of if they're "live".
Note some operations in JavaFX will first check that they were invoked on the JavaFX Application Thread, throwing an IllegalStateException
if not. But many operations will not perform this check.
Addressing Your Question
You asked:
What's the best way to achieve above in thread safe manner?
If you only need to read that one TextField
in the middle of your background task, then a quick and somewhat dirty solution is to use CompletableFuture
. Replace:
String result = textField.getText();
With:
String result =
CompletableFuture
.supplyAsync(textField::getText, Platform::runLater)
.join();
But it also seems like what you have is two separate tasks, where the second depends on the result of the first. In that case, use two implementations of Task
and start the second one when the first one succeeds. This approach is demonstrated in the answer by @James_D. Also see Concurrency in JavaFX.
Is Reading UI State on a Background Thread Safe?
The answer is basically, "no, but also it kind of depends".
Short Answer
No, reading UI state on a background thread is not safe. Just act like that's always true and I would be surprised if you can go wrong. Whereas trying to reason if one particular scenario is thread-safe can easily lead to mistakes or break in the future.
Long Answer
In the simplest case, reading UI state on a background thread is unlikely to cause any undesirable side-effects. For example, the following application:
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();
}
}
Should be thread-safe, if I'm not mistaken. The label's text property is set before starting the background thread. That is to say, setting the text property happens-before starting the background thread. Therefore, the background thread should always read the latest value (i.e., not read a stale value).
Now, one might think this sort of thing is always safe because you're only reading state, not writing state. But you can't always be sure that calling a method will definitely not cause any writes. For instance, the text property of the label inherits from StringPropertyBase
(implementation detail). And that class maintains a "valid" state. When you call getText()
you end up causing valid = true
to be executed. Alert! That's a write! Is that likely to cause problems? Probably not in this specific case. But the point is you don't always know what side-effects something as innocuous as reading some state will cause (unless the documentation explains otherwise).
Even if there are no writes, you might still end up reading a stale value. Say your user is trying to delete a directory and they choose the wrong one. They fix it before starting the delete action, but somehow your task reads the old, incorrect directory and deletes that one instead. Given the nature of UI interaction, my intuition says this scenario is unlikely if not impossible, but imagine your user's horror at irrecoverably deleting their family photos instead of some old documents. And their anger when they realize this was caused by a bug in your application. Better to just code your application so this cannot possibly happen unless by user error.
In conclusion, you should assume reading UI state from a background thread is unsafe. This is good practice in general Java programming, too. Assume an object is not thread-safe unless explicitly documented otherwise; always ensure that a happens-before relationship is created when communicating between threads.
Besides, it's not any harder to write the following 100% thread-safe code:
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();
}
}
Answered By - Slaw
Answer Checked By - Mary Flores (JavaFixing Volunteer)