问题
TL;DR I'm looking for a way to have one thread raise an event in another
EDIT: I say the word "immediate", which is, as some commenters have pointed out, impossible. What I mean is that it should happen reasonably quickly, in the low milli to nanosecond range if the gui thread is idle (which , if I do my job right, it should be).
The case example: I have a project which has a Parent class. That Parent class creates a child thread 'Gui', which houses a javafx application and implements Runnable. Both Parent and Gui have a reference to the same BlockingQueue.
What I want to happen: I want to be able to send objects from the parent class to the Gui thread, and have the Gui receive some sort of event which immediately calls a handling function of some sort, so I then know to get one or more objects from the queue and add them to the gui.
Other solutions for the "observer pattern" typically involve an observer which sits in a while loop, checking some synchronized queue for new data. This won't work for my application because Javafx requires that gui elements be modified only from the gui thread, and that the gui thread must largely be left unoccupied, so that it has time to redraw things and respond to user events. A loop would cause the application to hang.
One idea that I've found which seems to have potential is to interrupt the Gui thread from the parent thread, and have that trigger some sort of event, but I couldn't find any way to make that happen.
Any ideas? What are best practices for this sort of situation?
回答1:
Really all it sounds like you need here is to invoke the update to the UI on the FX Application Thread via Platform.runLater(...)
. This will schedule an update which will be executed as soon as the FX Application Thread has time, which will be pretty quick as long as you are not flooding it with too many requests. The update will be visible to the user the next time a rendering pulse occurs (so from the user's perspective this happens as soon as is possible).
Here is an example in it's barest sense: the asynchronous class producing the data directly schedules the updates on the UI.
First a simple class to hold some data. I added in some functionality for checking the "age" of the data, i.e. how long since the constructor was invoked:
MyDataClass.java
public class MyDataClass {
private final int value ;
private final long generationTime ;
public MyDataClass(int value) {
this.value = value ;
this.generationTime = System.nanoTime() ;
}
public int getValue() {
return value ;
}
public long age() {
return System.nanoTime() - generationTime ;
}
}
Here's a simple UI that displays all data it receives, along with the "age" of the data and an average of all data:
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.Parent;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;
public class UI {
private final TextArea textArea ;
private final Parent view ;
private long total ;
private long count ;
private final DoubleProperty average = new SimpleDoubleProperty(0);
public UI() {
textArea = new TextArea();
Label aveLabel = new Label();
aveLabel.textProperty().bind(average.asString("Average: %.3f"));
view = new BorderPane(textArea, null, null, aveLabel, null);
}
public void registerData(MyDataClass data) {
textArea.appendText(String.format("Data: %d (received %.3f milliseconds after generation)%n",
data.getValue(), data.age()/1_000_000.0));
count++;
total+=data.getValue();
average.set(1.0*total / count);
}
public Parent getView() {
return view ;
}
}
Here's a class that (asynchronously) sleeps a lot and produces random data (kind of like my interns...). For now, it just has a reference to the UI so it can schedule updates directly:
import java.util.Random;
import javafx.application.Platform;
public class DataProducer extends Thread {
private final UI ui ;
public DataProducer(UI ui) {
this.ui = ui ;
setDaemon(true);
}
@Override
public void run() {
Random rng = new Random();
try {
while (true) {
MyDataClass data = new MyDataClass(rng.nextInt(100));
Platform.runLater(() -> ui.registerData(data));
Thread.sleep(rng.nextInt(1000) + 250);
}
} catch (InterruptedException e) {
// Ignore and allow thread to exit
}
}
}
And finally here's the application code:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class AsyncExample extends Application {
@Override
public void start(Stage primaryStage) {
UI ui = new UI();
DataProducer producer = new DataProducer(ui);
producer.start();
Scene scene = new Scene(ui.getView(), 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Running this, I see the data being processed by the UI around 0.1 milliseconds after it is generated, which meets your requirements. (The first one or two will take longer, as they are generated before the start method completes and before the UI is physically displayed, so their calls to Platform.runLater(...)
will need to wait for that work to complete.)
The issue with this code is, of course, that the DataProducer
is tightly coupled to the UI and to JavaFX (using the Platform
class directly). You can remove this coupling by giving it a general consumer to process the data:
import java.util.Random;
import java.util.function.Consumer;
public class DataProducer extends Thread {
private final Consumer<MyDataClass> dataConsumer ;
public DataProducer(Consumer<MyDataClass> dataConsumer) {
this.dataConsumer = dataConsumer ;
setDaemon(true);
}
@Override
public void run() {
Random rng = new Random();
try {
while (true) {
MyDataClass data = new MyDataClass(rng.nextInt(100));
dataConsumer.accept(data);
Thread.sleep(rng.nextInt(1000) + 250);
}
} catch (InterruptedException e) {
// Ignore and allow thread to exit
}
}
}
and then
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class AsyncExample extends Application {
@Override
public void start(Stage primaryStage) {
UI ui = new UI();
DataProducer producer = new DataProducer(d -> Platform.runLater(() -> ui.registerData(d)));
producer.start();
Scene scene = new Scene(ui.getView(), 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Note that setting a Consumer
here is very similar to providing an event handler: the consumer is "notified" or "triggered" whenever a data element is generated. You could easily extend this to have a List<Consumer<MyDataClass>>
if you wanted multiple different views to be notified, and add/remove consumers to that list. The data type MyDataClass
plays the role of the event object: it contains the information about exactly what happened. Consumer
is a general functional interface, so it can be implemented by any class you choose, or by a lambda expression (as we do in this example).
As a variant on this version, you can decouple the Platform.runLater(...)
from the execution of the Consumer
by abstracting Platform.runLater(...)
as a java.util.concurrent.Executor
(which is just something that runs Runnable
s):
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
public class DataProducer extends Thread {
private final Consumer<MyDataClass> dataConsumer ;
private final Executor updateExecutor ;
public DataProducer(Consumer<MyDataClass> dataConsumer, Executor updateExecutor) {
this.dataConsumer = dataConsumer ;
this.updateExecutor = updateExecutor ;
setDaemon(true);
}
@Override
public void run() {
Random rng = new Random();
try {
while (true) {
MyDataClass data = new MyDataClass(rng.nextInt(100));
updateExecutor.execute(() -> dataConsumer.accept(data));
Thread.sleep(rng.nextInt(1000) + 250);
}
} catch (InterruptedException e) {
// Ignore and allow thread to exit
}
}
}
and
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class AsyncExample extends Application {
@Override
public void start(Stage primaryStage) {
UI ui = new UI();
DataProducer producer = new DataProducer(ui::registerData, Platform::runLater);
producer.start();
Scene scene = new Scene(ui.getView(), 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
An alternative way to decouple the classes is to use a BlockingQueue
to transmit the data. This has the feature that you can limit the size of the queue, so the data producing thread will block if there is too much data pending. Additionally, you can "bulk process" many data updates in the UI class, which is useful if you are producing them quickly enough to flood the FX Application Thread with too many updates (I don't show that code here; you would need to consume the data in an a AnimationTimer
and further relax your notion of "immediate"). This version looks like:
import java.util.Random;
import java.util.concurrent.BlockingQueue;
public class DataProducer extends Thread {
private final BlockingQueue<MyDataClass> queue ;
public DataProducer(BlockingQueue<MyDataClass> queue) {
this.queue = queue ;
setDaemon(true);
}
@Override
public void run() {
Random rng = new Random();
try {
while (true) {
MyDataClass data = new MyDataClass(rng.nextInt(100));
queue.put(data);
Thread.sleep(rng.nextInt(1000) + 250);
}
} catch (InterruptedException e) {
// Ignore and allow thread to exit
}
}
}
The UI has a bit more work to do: it needs a thread to repeatedly take elements from the queue. Note that queue.take()
blocks until there is an element available to take:
import java.util.concurrent.BlockingQueue;
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.Parent;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;
public class UI {
private final TextArea textArea ;
private final Parent view ;
private long total ;
private long count ;
private final DoubleProperty average = new SimpleDoubleProperty(0);
public UI(BlockingQueue<MyDataClass> queue) {
textArea = new TextArea();
Label aveLabel = new Label();
aveLabel.textProperty().bind(average.asString("Average: %.3f"));
view = new BorderPane(textArea, null, null, aveLabel, null);
// thread to take items from the queue and process them:
Thread queueConsumer = new Thread(() -> {
while (true) {
try {
MyDataClass data = queue.take();
Platform.runLater(() -> registerData(data));
} catch (InterruptedException exc) {
// ignore and let thread exit
}
}
});
queueConsumer.setDaemon(true);
queueConsumer.start();
}
public void registerData(MyDataClass data) {
textArea.appendText(String.format("Data: %d (received %.3f milliseconds after generation)%n",
data.getValue(), data.age()/1_000_000.0));
count++;
total+=data.getValue();
average.set(1.0*total / count);
}
public Parent getView() {
return view ;
}
}
and then you just do
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class AsyncExample extends Application {
private final int MAX_QUEUE_SIZE = 10 ;
@Override
public void start(Stage primaryStage) {
BlockingQueue<MyDataClass> queue = new ArrayBlockingQueue<>(MAX_QUEUE_SIZE);
UI ui = new UI(queue);
DataProducer producer = new DataProducer(queue);
producer.start();
Scene scene = new Scene(ui.getView(), 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Again, all these versions simply work by using Platform.runLater(...)
to schedule the updates (there are just varying mechanisms of decoupling the classes). What this actually does, at least conceptually, is place the runnable into an unbounded queue; the FX Application Thread takes elements from this queue and runs them (on that thread). Thus the runnable is executed as soon as the FX Application Thread has a chance, which is really as much as you can achieve.
It doesn't sound like you need the thread that is producing the data to block until the data has been processed, but that can be achieved too if you need (as an example, just set the queue size to 1).
回答2:
Read the question and answer here:(refresh label not working correctly javafx)
Read the question and answer here:(Queue print jobs in a separate single Thread for JavaFX)
The above are questions and answers using BlockingQueue
.
Tutorial and Theory here:http://tutorials.jenkov.com/java-util-concurrent/blockingqueue.html
来源:https://stackoverflow.com/questions/39627070/triggering-asynchronous-event-in-gui-thread