TL; DR我正在寻找让一个线程在另一个线程中引发事件的方法
编辑:我说“立即”这个词,就像一些评论者指出的那样,是不可能的。我的意思是它应该合理地快速发生,如果gui线程处于空闲状态(如果我的工作正确,应该是),在毫秒到纳秒范围内。(/ p>
案例: 我有一个有Parent类的项目。该Parent类创建一个子线程'Gui',它包含一个javafx应用程序并实现Runnable。 Parent和Gui都引用了相同的BlockingQueue。
我想要发生什么: 我希望能够将对象从父类发送到Gui线程,并让Gui接收某种事件,该事件立即调用某种处理函数,因此我知道从队列中获取一个或多个对象将它们添加到gui。
“观察者模式”的其他解决方案通常涉及一个位于while循环中的观察者,检查一些同步队列中的新数据。这对我的应用程序不起作用,因为Javafx要求仅从gui线程修改gui元素,并且gui线程必须在很大程度上保持空闲状态,以便它有时间重绘事物并响应用户事件。循环会导致应用程序挂起。
我发现似乎有潜力的一个想法是从父线程中断Gui线程,并触发某种事件,但我找不到任何方法来实现这一点。
有什么想法吗?对于这种情况,最佳做法是什么?
答案 0 :(得分:2)
这里听起来你需要的是通过Platform.runLater(...)
调用FX应用程序线程上的UI更新。这将安排一个更新,该更新将在FX应用程序线程有时间后立即执行,只要您没有充斥太多请求,这将非常快。下次渲染脉冲发生时,用户可以看到更新(因此从用户的角度来看,这种情况会尽快发生)。
这是一个最简单的例子:生成数据的异步类直接调度UI上的更新。
首先是一个保存一些数据的简单类。我添加了一些功能来检查" age"数据,即调用构造函数后的时间:
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 ;
}
}
这是一个简单的用户界面,可显示收到的所有数据,以及"年龄"数据和所有数据的平均值:
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 ;
}
}
这是一个(异步)睡眠很多并产生随机数据的类(有点像我的实习生......)。目前,它只是对UI的引用,因此它可以直接安排更新:
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
}
}
}
最后是应用程序代码:
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);
}
}
运行它,我看到UI生成后大约0.1毫秒处理的数据符合您的要求。 (前一个或两个将花费更长时间,因为它们是在start方法完成之前和UI物理显示之前生成的,因此他们对Platform.runLater(...)
的调用将需要等待该工作完成。)
此代码的问题当然是DataProducer
与UI和JavaFX紧密耦合(直接使用Platform
类)。您可以通过为一般消费者提供处理数据来删除此耦合:
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
}
}
}
然后
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);
}
}
请注意,此处设置Consumer
与提供事件处理程序非常相似:消费者被通知&#34;或者&#34;触发&#34;每当生成数据元素时。如果您希望通知多个不同的视图,则可以轻松地将其扩展为List<Consumer<MyDataClass>>
,并将消费者添加/删除到该列表。数据类型MyDataClass
扮演事件对象的角色:它包含有关所发生事件的确切信息。 Consumer
是一个通用的功能接口,因此它可以由您选择的任何类实现,也可以由lambda表达式实现(如本例中所示)。
作为此版本的变体,您可以通过将Platform.runLater(...)
抽象为Consumer
(这只是运行的东西)将Platform.runLater(...)
与java.util.concurrent.Executor
的执行分离开来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
}
}
}
和
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);
}
}
解耦类的另一种方法是使用BlockingQueue
来传输数据。这具有可以限制队列大小的功能,因此如果有太多数据待处理,数据生成线程将阻塞。此外,您可以批量处理&#34; UI类中的许多数据更新,如果您足够快地生成它们以使更多的更新涌入FX应用程序线程,这将非常有用(我不会在此处显示该代码;您需要在a中使用数据) AnimationTimer
并进一步放松你的概念&#34;立即&#34;)。这个版本看起来像:
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
}
}
}
UI还有一些工作要做:它需要一个线程来重复从队列中获取元素。请注意,queue.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 ;
}
}
然后你就做了
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);
}
}
同样,所有这些版本只需使用Platform.runLater(...)
来安排更新(只有不同的解耦类机制)。实际上,这至少在概念上是将runnable放入一个无界的队列中; FX Application Thread从此队列中获取元素并运行它们(在该线程上)。因此,一旦FX应用程序线程有机会执行runnable,这实际上就是你可以实现的。
听起来你需要生成要阻塞的数据的线程,直到数据处理完毕,但是如果你需要的话也可以实现(例如,只需将队列大小设置为1) )。
答案 1 :(得分:1)
在此处阅读问题和答案:(refresh label not working correctly javafx)
在此处阅读问题和答案:(Queue print jobs in a separate single Thread for JavaFX)
以上是使用BlockingQueue
的问题和答案。
这里的教程和理论:http://tutorials.jenkov.com/java-util-concurrent/blockingqueue.html