JavaFX 中使用多线程与保证 UI 线程安全

JavaFX 中使用多线程与保证 UI 线程安全

  UI 线程指的是直接控制 UI 界面变化的线程。JavaFX 中的 UI 线程和大多数其它的编程语言一样,是单线程的。前人很早就已经多次尝试在 UI 线程上使用多线程,大多都已失败告终。

  单线程的好处在于编程方便,在同一线程中无需考虑并发死锁等问题。从某种角度上,单线程的效率要比多线程高,因为它避免了频繁的上下方切换以及数据竞争,比如著名的 NoSQL 数据库 Redis 就是单线程的,但这要依情况而定。一种广泛流行的错误观点是,多线程一定优于单线程。如果有人认为这种观点是正确的,不妨试试让每条代码都通过新开一个线程来执行。多线程程序是否优于单线程,这要由问题模型以及编程人员的能力决定。无前提条件地认为多线程一定优于单线程是错误的。


【提示】

  不仅仅是 JavaFX 中的 UI 线程是单线程的,很多其它领域中 UI 线程都是单线程的,如 Java Swing、浏览器中 JavaScript 控制下的 UI、安卓中的 UI 线程。

  很多领域中 UI 线程都是单线程的,而且还是非线程安全的。如果一个 UI 环境下的编程语言支持多线程(如 JavaFX、安卓等),那么这就意味着程序中的某些代码就有可能不是在 UI 线程中执行,而在用户自己新建的线程中执行。一般来说,此新建线程不会合并到 UI 线程中,而是一种真实地独立于 UI 线程的线程。稍后就会解释,UI 线程不能执行非常耗时的操作。为了保证 UI 界面的流畅,在很多领域中 UI 同时还是非线程安全的,原因是保证 UI 线程安全会为 UI 线程带来额外的开销,这会影响 UI 界面的流畅。因此,当这些 UI 环境下的编程语言支持多线程,那么在用户自己新建的线程中如果需要更新 UI,则需要将所有更新 UI 的代码通过函数式编程技术置于一种 UI 安全函数。很多支持多线程的 UI 环境都有提供了这种 UI 安全函数,如 JavaFX 的 Platform.runLater


  为保证 UI 界面的流畅,UI 线程不能执行非常耗时的操作。如果 UI 线程执行正在非常耗时的操作,这个后果在 UI 界面的体现就是,UI 界面会一直停滞在执行耗时代码前的状态,然后如果马上随意连续点击 UI 界面的任何部位,此时会发生如下现象:

  • 应用的标题会加上 (未响应) 的后缀。

  • 应用的关闭按钮会变红。

  • 光标位于此应用中时,光标会变成加载的圆圈图样。

  • 操作系统会将此应用的界面变成灰色,然后弹窗提示此程序已停止响应。

  上面的就是俗称应用卡死的状态。通过上面的描述应该可以明白,不是说一旦进入卡死的状态,就只能手动强制结束这个应用。应用卡死的状态只能一种 UI 界面被阻塞的状态(UI 界面无法自主更新)。当发生了这个状态,并不能说明程序就发生了死锁,因此此时如果等待,程序就有可能自主走出这个状态。只能说这个应用的开发者的设计不合理,UI 线程不应该执行非常耗时的操作。那么,非常耗时的操作应该在哪里执行呢?


【注意】

  UI 界面的更新是以异步的方式进行。UI 线程首先会执行用户代码,然后如果这些代码使得 UI 界面的数据发生的改变,UI 线程将令操作系统将对其 UI 界面进行更新。这意味着,并不是每执行一条更改 UI 数据的代码,它都会在 UI 界面上马上生效。有时候,这会导致一些问题。

  UI 线程是单线程的,指的 UI 界面是只通过一个线程来完成它界面的更新,指的不是凡是涉及 UI 的程序只能使用一个线程。UI 应用相比于后台应用,只是多了几个与处理 UI 相关的线程而已,没什么额外的线程个数限制。

  如果想了解更多关于同步、异步、阻塞的知识,可见笔者的另一篇博客:

同步阻塞、同步非阻塞、异步阻塞、异步非阻塞:
https://blog.csdn.net/wangpaiblog/article/details/117236684


JavaFX 中使用多线程

  为防止 UI 界面被阻塞,又因为 UI 线程是单线程的,因此应该选择在其它线程执行非常耗时的操作。可以选择当需要执行非常耗时的操作时,新开一个线程,将此非常耗时的操作放到新开一个线程去执行。

  在 JavaFX 中使用多线程一般使用两个类:ExecutorServiceTask<Integer>Task<Integer> 有一个方法 call,可以在这个方法去执行耗时操作。具体代码如下:

// 假设方法 someJavafxFun 位于 JavaFX 的某个组件的定义中
public void someJavafxFun() {
    ExecutorService executor = Executors.newCachedThreadPool();
    Task<Integer> task = new Task<>() {
        @Override
        protected Integer call() {
            // TODO 执行耗时操作
            return null; // 如果需要结果反馈,可以在此处提供反馈值
        }
    };
    /**
     * 如果不需要结果反馈,也可以直接使用 executor.execute(task);
     * 
     * 可以使用 result.get() 来获取上面的反馈值。但这个方法是同步阻塞的
     */
    var result = executor.submit(task);

    /**
     * 方法 getWindow() 获得的其实是 Stage。此段代码是用于在应用关闭时回收资源。
     * 
     * 对于真正的程序,方法 setOnCloseRequest 要设置在 Stage 被创建处。
     * 因为方法 setOnCloseRequest 会覆盖其它 setOnCloseRequest 的效果,所以此方法只能执行一次。
     * 为了达到这个效果,需要将 task 与 executor 设置成全局的,或者将其封装在一个全局静态方法中
     */
    this.getScene().getWindow().setOnCloseRequest(event -> {
        if (task != null) {
            task.cancel();
        }
        if (executor != null) {
            executor.shutdown();
        }
        Platform.exit();
    });
}

  Task<T> 是 JavaFX 的一个类,它继承至 FutureTask<T>。而 FutureTask<T>ExecutorService 均为原生的 Java 多线程中的类,后续的操作均可依照 Java 多线程理论中的流程来完成。


【附】

  可以使用 Thread.currentThread().getName() 来查看某代码位于的线程。如下。其中,JavaFX 的 UI 界面所在的线程名为 JavaFX Application Thread

System.out.println("【编号xxx】 执行本代码 XXX 的线程是:" + Thread.currentThread().getName());

JavaFX 中保证 UI 线程安全

  JavaFX 中的 UI 和大多数其它的编程语言中的一样,不是线程安全的,因为它是单线程的。在单线程中无需考虑线程安全的问题,但在多线程中需要考虑。介于本文讨论的重点,这里不打算解释什么是线程安全。那么,如果在 JavaFX 中使用了多线程,如何保证 UI 线程安全呢?

  在 JavaFX 中,可以在 UI 之外的线程中,使用方法 Platform.runLater 来执行与 UI 直接相关的操作。如下:

Platform.runLater(() -> {/* // TODO 更新 UI 数据的代码 */});

  注意:为保证 UI 界面的流畅,只需将与 UI 直接相关的代码置入上述的方法 Platform.runLater 中,不要在此方法中放多余的代码,否则就失去了使用多线程的意义。

总结与补充

  • 为保证 UI 界面的流畅,需要将某些代码放入新建线程中,这些代码需要同时满足以下条件:

    • 非常耗时或执行时间不能保证最坏结果也符合要求

    • 与操作 UI 数据不直接相关

    • 并非与 UI 数据强同步。

      例如:如果 UI 需要请求一个资源,如果该资源不能获得,UI 就会崩溃,那么获取该资源的代码不能放在新建线程中,除非可以保证此线程与 UI 线程可以同步。因为,在不使用任何机制的情况下,新建的线程都是非阻塞的,如果选择将获取该资源的代码放在新建线程中,在这种情况下,UI 中请求资源的方法会立即返回,这个时候获取到的是这个资源的初始值(一般是 null)。也就是说,如果选择将获取该资源的代码放在新建线程中,相当于直接注释掉了新建线程获取资源这部分的代码。

  • 为保证 UI 的线程安全,在其它线程不能直接更改 UI 的数据,必须将更改 UI 数据的代码传于方法 Platform.runLater 中运行。

  • 5
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
你可以按照以下步骤来编写一个简单的计算器程序: 1. 创建一个新的 JavaFX 项目并添加一个新的 FXML 文件。 2. 在 FXML 文件添加一个 GridPane,用于放置计算器按钮和显示结果。 3. 在 GridPane 添加一个 TextField,用于显示计算器输出。 4. 在 GridPane 添加多个 Button,用于表示数字和操作符。 5. 在 Controller 类创建一个变量,用于存储当前计算器的状态。 6. 在 Controller 类添加事件处理程序,用于响应用户点击按钮的操作。 7. 在事件处理程序更新计算器状态,并根据用户的操作更新 TextField 的文本。 8. 最后,将 FXML 文件与 Controller 类关联起来,并在主应用程序启动计算器。 下面是一个简单的 JavaFX 计算器示例,供你参考: FXML 文件: ```xml <?xml version="1.0" encoding="UTF-8"?> <?import javafx.scene.control.Button?> <?import javafx.scene.control.TextField?> <?import javafx.scene.layout.GridPane?> <GridPane fx:controller="com.example.calculator.Controller" xmlns:fx="http://javafx.com/fxml" alignment="CENTER"> <TextField fx:id="resultField" GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.columnSpan="4" editable="false" promptText="0" style="-fx-font-size: 18px;" /> <Button text="7" onAction="#handleNumberClick" GridPane.columnIndex="0" GridPane.rowIndex="1" /> <Button text="8" onAction="#handleNumberClick" GridPane.columnIndex="1" GridPane.rowIndex="1" /> <Button text="9" onAction="#handleNumberClick" GridPane.columnIndex="2" GridPane.rowIndex="1" /> <Button text="/" onAction="#handleOperatorClick" GridPane.columnIndex="3" GridPane.rowIndex="1" /> <Button text="4" onAction="#handleNumberClick" GridPane.columnIndex="0" GridPane.rowIndex="2" /> <Button text="5" onAction="#handleNumberClick" GridPane.columnIndex="1" GridPane.rowIndex="2" /> <Button text="6" onAction="#handleNumberClick" GridPane.columnIndex="2" GridPane.rowIndex="2" /> <Button text="*" onAction="#handleOperatorClick" GridPane.columnIndex="3" GridPane.rowIndex="2" /> <Button text="1" onAction="#handleNumberClick" GridPane.columnIndex="0" GridPane.rowIndex="3" /> <Button text="2" onAction="#handleNumberClick" GridPane.columnIndex="1" GridPane.rowIndex="3" /> <Button text="3" onAction="#handleNumberClick" GridPane.columnIndex="2" GridPane.rowIndex="3" /> <Button text="-" onAction="#handleOperatorClick" GridPane.columnIndex="3" GridPane.rowIndex="3" /> <Button text="0" onAction="#handleNumberClick" GridPane.columnIndex="0" GridPane.rowIndex="4" GridPane.columnSpan="2" /> <Button text="." onAction="#handleNumberClick" GridPane.columnIndex="2" GridPane.rowIndex="4" /> <Button text="+" onAction="#handleOperatorClick" GridPane.columnIndex="3" GridPane.rowIndex="4" /> <Button text="C" onAction="#handleClearClick" GridPane.columnIndex="0" GridPane.rowIndex="5" GridPane.columnSpan="2" /> <Button text="=" onAction="#handleEqualClick" GridPane.columnIndex="2" GridPane.rowIndex="5" GridPane.columnSpan="2" /> </GridPane> ``` Controller 类: ```java package com.example.calculator; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.TextField; public class Controller { private enum Operator { ADD, SUBTRACT, MULTIPLY, DIVIDE } private Operator currentOperator; private double currentValue; private boolean startNewValue = true; @FXML private TextField resultField; @FXML public void handleNumberClick(ActionEvent event) { String digit = ((Button) event.getSource()).getText(); if (startNewValue) { resultField.setText(digit); startNewValue = false; } else { resultField.setText(resultField.getText() + digit); } } @FXML public void handleOperatorClick(ActionEvent event) { String operatorSymbol = ((Button) event.getSource()).getText(); switch (operatorSymbol) { case "+": currentOperator = Operator.ADD; break; case "-": currentOperator = Operator.SUBTRACT; break; case "*": currentOperator = Operator.MULTIPLY; break; case "/": currentOperator = Operator.DIVIDE; break; } currentValue = Double.parseDouble(resultField.getText()); startNewValue = true; } @FXML public void handleClearClick(ActionEvent event) { resultField.setText("0"); startNewValue = true; } @FXML public void handleEqualClick(ActionEvent event) { double newValue = Double.parseDouble(resultField.getText()); switch (currentOperator) { case ADD: currentValue += newValue; break; case SUBTRACT: currentValue -= newValue; break; case MULTIPLY: currentValue *= newValue; break; case DIVIDE: currentValue /= newValue; break; } resultField.setText(String.valueOf(currentValue)); startNewValue = true; } } ``` 在主应用程序启动计算器: ```java package com.example.calculator; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; public class Main extends Application { @Override public void start(Stage primaryStage) throws Exception { Parent root = FXMLLoader.load(getClass().getResource("calculator.fxml")); primaryStage.setTitle("Calculator"); primaryStage.setScene(new Scene(root, 300, 400)); primaryStage.show(); } public static void main(String[] args) { launch(args); } } ``` 运行程序,你应该可以看到一个简单的计算器界面。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值