JavaFX 前台/后台消息传递
该程序演示了JavaFX前台与一个或多个后台线程之间进行双向消息传递所需的方法。
引言
JavaFX 由于内存寻址限制,不允许后台线程直接操作屏幕数据。
只有JavaFX前台线程可以进行屏幕更改。后台线程必须设法通知FX前台有可用的数据需要处理。
该程序演示了
- 如何实现后台需要将数据传递给FX前台
- 将数据从FX前台传递到后台线程的技术
- 避免FX前台在后台被阻塞的技术
- 完全异步前台/后台处理的实现
该程序使用标准的Java Scene Builder应用程序创建单个用户屏幕。
该程序是使用JavaFX的MVC模型开发的。
在使用JavaFX时,每个屏幕布局都存储在单独的fxml文件中。
该程序的屏幕布局在SimpleFXBG.fxml中。
JavaFX程序的主线类部分相对较小。
该程序的主线代码类在SimpleFXBG.java中。
屏幕初始化和事件处理程序为每个屏幕使用一个控制器类部分。
该程序的控制器是SimpleFXBGController.java。
控制器的类数据是
@FXML Button buttonSend;
@FXML Button buttonStop;
@FXML ListView listMsgs;
@FXML TextField textToSend;
private ObservableList<String> msgData;
private SimpleBG simpleBG;
@FXML
标签是与屏幕关联的字段,其属性在fxml文件中指定。
用于处理buttonSend
控件的事件处理程序是handleSendMessage
,当用户希望向后台发送消息时,该处理程序会被激活。
将要发送到后台的消息包含在textTosend
文本属性中。
buttonStop
屏幕控件的事件处理程序用于关闭程序。
FX的ListView
用于在FX屏幕控件上显示来自后台的消息。
FX的ListView
控件不会直接通过添加新行来修改,而是通过修改ObservableList
来添加新的后台消息。
在屏幕控件构造函数实例化时,ObservableList
会绑定到ListView
屏幕控件。
屏幕控件的属性初始化不多,因为属性已通过JavaFX Scene Builder预先定义并放在fxml文件中。
屏幕控制器构造函数现在简化为
msgData = FXCollections.observableArrayList();
listMsgs.setItems(msgData);
simpleBG = new SimpleBG(this);
构造函数创建一个FXCollections.observableArrayList()
,用于在屏幕上显示来自后台线程的消息。
接下来,FXCollections.observableArrayList()
被绑定到屏幕上的ListView listMsgs
。
最后,创建后台类的实例,并将屏幕控制器的实例传递给后台类。
必须将屏幕控制器传递给后台,以便后台知道在哪里发送直接发往FX前台的后台消息。
值得注意的是,在FX前台屏幕控制器中没有创建任何队列,并且可能相关的等待。
从FX前台发送消息到后台是在Button
事件处理程序的事件处理程序中完成的。
@FXML
// Send message to background
private void handleSendAction(ActionEvent event)
{
int queueDepth = simpleBG.receiveMsg(textToSend.getText());
} // private void handleSendAction(...)
事件处理程序中的唯一操作是将消息发送到在FX屏幕控制器构造函数中创建的后台对象。
此调用不应在后台阻塞。
当后台有发往FX前台的消息时,后台调用FX控制器的onMessage
。
// Handle message from background
public void onMessage(String argMsgToDisplay)
{
msgData.add(argMsgToDisplay);
} // pubic void onMessage(...)
onMessage
事件处理程序的唯一操作是将后台消息添加到observableArrayList
。
访问此方法是为什么将FX控制器实例化传递给后台构造函数的原因。
FX前台控制器上的所有操作现已定义,现在必须检查后台。
SimpleBG.java
后台由4个类组成
- SimpleBG.java
- SimpleBGConsumer.java
- SimpleBGProducer.java
- SimpleBGSender.java
SimpleBG.java
SimpleBG
创建后台使用的共享资源。
它还实例化了其他三个后台类,并将必要的共享资源传递给每个类。
private final SimpleBGProducer producer;
private final SimpleBGConsumer consumer;
private final SimpleBGSender sender;
private final ArrayBlockingQueue<String> outboundMsgQueue;
private final ArrayBlockingQueue<String> inboundMsgQueue;
为了处理FX前台和后台之间的消息队列,创建了两个ArrayBlockingQueue
,每个方向一个。
outboundMsgQueue
队列化从后台发往FX前台的消息,而inboundMsgQueue
队列化消息的另一个方向。
这些队列是潜在的线程阻塞的第一个线索。
类构造函数需要FX类控制器,如FX类构造函数前面所述。
public SimpleBG(SimpleFXBGController argController)
{
// Create Q for consumer thread that may block
inboundMsgQueue = new ArrayBlockingQueue<>(QUEUE_DEPTH, true);
// Create Q for sender and producer
outboundMsgQueue = new ArrayBlockingQueue<>(QUEUE_DEPTH, true);
// Create consumer of messages from foreground
consumer = new SimpleBGConsumer(inboundMsgQueue);
// Create sender before producer
sender = new SimpleBGSender(argController, outboundMsgQueue);
// Create producer of message to foreground
producer = new SimpleBGProducer(outboundMsgQueue);
} // public SimpleBG(...)
创建了两个队列。
这两个对象有潜在的线程阻塞。
每个队列都以特定的最大长度创建。QUEUE_DEPTH
队列的true
选项被指定以保证消息的FIFO排序。
SimpleBGConsumer.java是用于从FX前台消耗消息的后台类。
创建时,消费者需要知道要查看哪个共享资源队列来消耗FX前台生成的消息。
SimpleBGSender.java是将消息发送到FX前台的后台类。
这是事件中断FX前台的后台类,发送方需要知道FX控制器是谁以及可以检索哪些队列中的消息来发往FX前台。
SimpleBGProducer.java是生成后台消息发往FX前台的后台类。
生产者需要知道持有它生成的、发往FX前台的消息的共享资源队列。
SimpleBG
中的另一个方法是
public int receiveMsg(String argInboundMsg)
{
// Restrict access to size & add are a joint atomic operation
synchronized(inboundMsgQueue)
{
int queueOccupancy;
// If Q full, don't block the FX foreground thread
if ((queueOccupancy = inboundMsgQueue.size()) == QUEUE_DEPTH)
{
return -1;
} // if ((queueOccupancy = inboundMsgQueue.size()) == QUEUE_DEPTH)
// Add message from FX foreground (just above checked for space)
inboundMsgQueue.add(argInboundMsg);
return ++queueOccupancy;
} // synchronized(argInboundMsg)
} // public int receiveMsg(...)
此方法由FX前台handleStopAction
事件处理程序调用。
传入的receiveMsg
是来自FX前台、将要被后台消耗的消息。
重要的是,当此方法从FX前台调用时,此方法不得阻塞FX前台线程。
尝试将FX前台消息放入已满的ArrayBlockingQueue
会使线程阻塞,这正是我们不希望对调用FX前台线程发生的情况。
为了防止阻塞,后台线程必须以某种方式检查队列是否已满。
然而,检查队列是否已满和将消息添加到队列是分开的两行代码,在多线程环境中,这两行需要被视为原子操作,即不可中断。
将这两行放在同步块中可以防止其他线程中断这两行。
现在这两行在同步块中,前台线程只需要等待很短的时间就可以获得对队列的独占访问权。
FX前台测试队列是否已满。
当队列已满时,FX前台调用返回-1
,表示队列已满。
返回时已满可防止阻塞调用将消息放入队列。
如果队列未满,则FX前台的消息将被放入消息队列以供消耗。
虽然此方法返回失败(-1
)或待消耗的消息数量,但只需要成功/非成功。
FX前台可以执行一些操作,例如在返回失败时弹出错误消息。
SimpleBGConsumer.java
SimpleBGConsumer
使用两个本地对象
private final Thread simpleBGConsumerThread;
private final ArrayBlockingQueue<String> inboundMsgQueue;
另一个对象是持有FX前台生成并发往该后台消费者线程的消息的队列。
public SimpleBGConsumer(ArrayBlockingQueue<String> argInboundMsgQueue)
{
inboundMsgQueue = argInboundMsgQueue;
// Thread can block waiting for data
simpleBGConsumerThread = new Thread(this);
simpleBGConsumerThread.start();
} // public SimpleBGConsumer()
消费者后台线程消耗消费者队列中的任何消息。如果队列中没有消息,消费者后台线程将阻塞。
这个简单的消费者只显示FX消息。
然而,它可以更复杂,例如,它可以将消息通过互联网发送。打开或关闭像灯或泵这样的设备,或者记录消息。
为了满足ArrayBlockingQueue
的使用,需要捕获InterruptedException
。
@Override
public void run()
{
String inboundMsg;
String text;
while (true)
{
try
{
// Wait for message
inboundMsg = inboundMsgQueue.take();
text = String.format("BG:'%s'", inboundMsg);
System.out.println(text);
} // try
catch (InterruptedException ie)
{
break;
} // catch (InterruptedException ie)
} // while (true)
} // public void run()
SimpleBGSender.java
SimpleBGSender
使用三个本地对象。
private final SimpleFXBGController controller;
private final Thread simpleBGSenderThread;
private final ArrayBlockingQueue<String> outBoundMsgQueue;
controller
是FX控制器对象。它从FX控制器传递到SimpleBG
构造函数,然后传递到这个类。
这提供了当后台消息可用时通知FX前台的门户。
simpleBGsenderThread
是此类的构造函数创建的后台线程。
是的,在构造函数中创建线程是糟糕的编程实践。
另一个对象是放入发往FX前台的后台生成消息的队列。
此类后台线程从共享队列outBoundMsgQueue
中获取消息,然后将它们发送到本地方法,该方法会中断FX前台并传递可用的后台生成的消息。
该线程阻塞等待outBoundMsgQueue
中的可用消息。
public void run()
{
String textToSend;
while(true)
{
try
{
// Wait for message to be sent to FX
textToSend = outBoundMsgQueue.take();
sendMsgToFX(textToSend);
} // try
catch (InterruptedException ie)
{
break;
} // catch (InterruptedException ie)
} // while(true)
} // public void run()
另一个本地方法负责中断FX前台以传递后台生成的消息。
传递给FX前台的参数必须声明为final,以满足FX前台的内存可访问性限制。
private void sendMsgToFX(final String argMsgToFX)
{
// Send msg to FX foreground thread
javafx.application.Platform.runLater
(
() ->
{
controller.onMessage(argMsgToFX);
} // () ->
); // javafx.application.Platform.runLater
} // private void sendMsgToFX(...)
SimpleBGProducer.java
SimpleBGProducer
使用三个本地对象。
private final ArrayBlockingQueue<String> outboundMsgQueue;
private final Thread simpleBGProducerThread;
private final int FIVE_SECONDS = 5000;
simpleBGProducerThread
是此类的构造函数创建的后台线程。
是的,在构造函数中创建线程是糟糕的编程实践。
outboundMsgQueue
对象是放入将发往FX前台的后台生成消息的队列。
最后一个对象FIVE_SECONDS
是后台线程消息之间的等待时间(以毫秒为单位)。
这是避免在代码中嵌入常量的良好编码实践。
与SimpleBGConsumer
类似,此类保存传递的队列并启动后台线程。
public SimpleBGProducer (ArrayBlockingQueue<String> argOutboundMsgQueue)
{
outboundMsgQueue = argOutboundMsgQueue;
// Thread can block of Q to FX is full
simpleBGProducerThread = new Thread(this);
simpleBGProducerThread.start();
} // public SimpleBGProducer (...)
后台线程是一个简单的循环,它暂停该线程,然后生成一条新消息并将其放入发往FX前台的消息队列中。
由于队列可能会因为已满而阻塞,因此必须处理InterruptedException
。
public void run()
{
String textToSend;
int msgSendNum = 0;
while(true)
{
try
{
// Generate new message every 5 seconds
Thread.sleep(FIVE_SECONDS);
textToSend = String.format("SM:Number %d", ++msgSendNum);
// Place in Q bound for FX foreground
outboundMsgQueue.add(textToSend);
} // try
catch (InterruptedException ie)
{
break;
} // catch (InterruptedException ie)
} // while(true)
} // public void run()
至此,JavaFX前台/后台应用程序结束。
它使用了各种高级功能,如JavaFX、多线程和阻塞数组。
使用了四个线程:隐式的FX前台,加上3个创建的后台线程:消费者、生产者和发送者。
本例传递的是String
消息,但可以传递更复杂的项。
创建了简单的消费者和生产者线程,但可以根据应用程序的需求修改它们的复杂性。
此方法不限于单个
public void onMessage(String argMsgToDisplay)
可以使用重载唯一数据类型或使用与onMessage
不同的唯一名称来编程后台生成消息的附加事件处理程序。
方法重载的示例可以是
public void onMessage(String argMsgToDisplay)
和
public void onMessage(MyClassObject argMsgToDisplay)
唯一名称的示例可以是
public void onMessageToList1(String argMsgToDisplay)
和
public void onMessageToList2(String argMsgToDisplay)
历史
- 2107/06/28:初始提交