JavaFX 的线程规则
线程和 JavaFX 有两个基本规则:
- 任何修改或访问作为场景图一部分的节点的状态的代码都必须在 JavaFX 应用程序线程上执行。某些其他操作(例如创建新操作)也受此规则的约束。
Stage
- 任何可能需要很长时间才能运行的代码都应在后台线程上执行(即不在 FX 应用程序线程上)。
第一条规则的原因是,与大多数 UI 工具包一样,编写框架时不会对场景图元素的状态进行任何同步。添加同步会产生性能开销,而这对于 UI 工具包来说是一个过高的成本。因此,只有一个线程可以安全地访问此状态。由于 UI 线程(JavaFX 的 FX 应用程序线程)需要访问此状态才能呈现场景,因此 FX 应用程序线程是唯一可以访问“实时”场景图形状态的线程。在 JavaFX 8 及更高版本中,受此规则约束的大多数方法都会执行检查,并在违反规则时引发运行时异常。(这与Swing相反,Swing可以编写“非法”代码,它可能看起来运行良好,但实际上容易在任意时间出现随机和不可预测的故障。这就是您看到的 IllegalStateException
的原因:您正在从 FX 应用程序线程以外的线程调用。courseCodeLbl.setText(...)
第二条规则的原因是,FX 应用程序线程除了负责处理用户事件外,还负责呈现场景。因此,如果对该线程执行长时间运行的操作,则在该操作完成之前不会呈现 UI,并且将对用户事件无响应。虽然这不会生成异常或导致对象状态损坏(违反规则 1 会),但它(充其量)会造成较差的用户体验。
因此,如果您有一个长时间运行的操作(如访问数据库)需要在完成时更新 UI,则基本计划是在后台线程中执行长时间运行的操作,在操作完成时返回操作的结果,然后在 UI(FX 应用程序)线程上计划对 UI 的更新。所有单线程UI工具包都有一个机制来做到这一点:在JavaFX中,你可以通过调用在FX应用程序线程上执行来做到这一点。(在 Swing 中,可以调用以在 AWT 事件调度线程上执行。JavaFX(请参阅本答案的后面部分)还提供了一些更高级别的API,用于管理与FX应用程序线程的通信。Platform.runLater(Runnable r)
r.run()
SwingUtilities.invokeLater(Runnable r)
r.run()
多线程处理的一般良好做法
使用多个线程的最佳做法是将要在“用户定义”线程上执行的代码构造为使用某种固定状态初始化的对象,具有执行操作的方法,并在完成后返回表示结果的对象。非常需要对初始化状态和计算结果使用不可变对象。这里的想法是尽可能消除任何可变状态从多个线程可见的可能性。从数据库访问数据非常适合这个习语:您可以使用数据库访问的参数(搜索词等)初始化“worker”对象。执行数据库查询并获取结果集,使用结果集填充域对象的集合,并在末尾返回该集合。
在某些情况下,需要在多个线程之间共享可变状态。当绝对必须这样做时,您需要仔细同步对该状态的访问,以避免在不一致的状态下观察状态(还有其他更微妙的问题需要解决,例如状态的活动性等)。当需要这样做时,强烈建议使用高级库来为您管理这些复杂性。
使用 javafx.concurrent API
JavaFX 提供了一个并发 API,该 API 设计用于在后台线程中执行代码,其 API 专门设计用于在完成(或期间)执行该代码时更新 JavaFX UI。此 API 旨在与 java.util.concurrent
API 交互,后者为编写多线程代码(但没有 UI 挂钩)提供了通用功能。中的关键类是 Task
,它表示打算在后台线程上执行的单个一次性工作单元。此类定义单个抽象方法,该方法不带任何参数,返回结果,并可能引发选中的异常。 实现其方法简单地调用 。 还有一组方法,这些方法保证在FX应用程序线程上更新状态,例如updateProgress(...)
,updateMessage(...)
等。它定义了一些可观察的属性(例如状态
和值
):这些属性的侦听器将收到有关FX应用程序线程上更改的通知。最后,有一些方便的方法来注册处理程序(setOnSucceed(...)
,setOnFailed(...)
等);通过这些方法注册的任何处理程序也将在 FX 应用程序线程上调用。javafx.concurrent
call()
Task
Runnable
run()
call()
Task
因此,从数据库中检索数据的一般公式是:
- 创建 用于处理对数据库的调用。
Task
- 使用执行数据库调用所需的任何状态初始化 。
Task
- 实现任务的方法以执行数据库调用,并返回调用的结果。
call()
- 向任务注册处理程序,以便在任务完成时将结果发送到 UI。
- 在后台线程上调用任务。
对于数据库访问,我强烈建议将实际的数据库代码封装在一个对 UI(数据访问对象设计模式)一无所知的单独类中。然后,只需让任务调用数据访问对象上的方法即可。
所以你可能有一个像这样的DAO类(注意这里没有UI代码):
public class WidgetDAO {
// In real life, you might want a connection pool here, though for
// desktop applications a single connection often suffices:
private Connection conn ;
public WidgetDAO() throws Exception {
conn = ... ; // initialize connection (or connection pool...)
}
public List<Widget> getWidgetsByType(String type) throws SQLException {
try (PreparedStatement pstmt = conn.prepareStatement("select * from widget where type = ?")) {
pstmt.setString(1, type);
ResultSet rs = pstmt.executeQuery();
List<Widget> widgets = new ArrayList<>();
while (rs.next()) {
Widget widget = new Widget();
widget.setName(rs.getString("name"));
widget.setNumberOfBigRedButtons(rs.getString("btnCount"));
// ...
widgets.add(widget);
}
return widgets ;
}
}
// ...
public void shutdown() throws Exception {
conn.close();
}
}
检索一堆小部件可能需要很长时间,因此来自UI类(例如控制器类)的任何调用都应在后台线程上调度。控制器类可能如下所示:
public class MyController {
private WidgetDAO widgetAccessor ;
// java.util.concurrent.Executor typically provides a pool of threads...
private Executor exec ;
@FXML
private TextField widgetTypeSearchField ;
@FXML
private TableView<Widget> widgetTable ;
public void initialize() throws Exception {
widgetAccessor = new WidgetDAO();
// create executor that uses daemon threads:
exec = Executors.newCachedThreadPool(runnable -> {
Thread t = new Thread(runnable);
t.setDaemon(true);
return t ;
});
}
// handle search button:
@FXML
public void searchWidgets() {
final String searchString = widgetTypeSearchField.getText();
Task<List<Widget>> widgetSearchTask = new Task<List<Widget>>() {
@Override
public List<Widget> call() throws Exception {
return widgetAccessor.getWidgetsByType(searchString);
}
};
widgetSearchTask.setOnFailed(e -> {
widgetSearchTask.getException().printStackTrace();
// inform user of error...
});
widgetSearchTask.setOnSucceeded(e ->
// Task.getValue() gives the value returned from call()...
widgetTable.getItems().setAll(widgetSearchTask.getValue()));
// run the task using a thread from the thread pool:
exec.execute(widgetSearchTask);
}
// ...
}
请注意,对(可能)长时间运行的 DAO 方法的调用如何包装在后台线程上运行的(通过访问器)中,以防止阻塞 UI(上面的规则 2)。对 UI () 的更新实际上是在 FX 应用程序线程上执行的,使用 的便利回调方法 setOnSucceed(...)
(满足规则 1)。Task
widgetTable.setItems(...)
Task
在您的情况下,您正在执行的数据库访问返回单个结果,因此您可能有一个类似
public class MyDAO {
private Connection conn ;
// constructor etc...
public Course getCourseByCode(int code) throws SQLException {
try (PreparedStatement pstmt = conn.prepareStatement("select * from course where c_code = ?")) {
pstmt.setInt(1, code);
ResultSet results = pstmt.executeQuery();
if (results.next()) {
Course course = new Course();
course.setName(results.getString("c_name"));
// etc...
return course ;
} else {
// maybe throw an exception if you want to insist course with given code exists
// or consider using Optional<Course>...
return null ;
}
}
}
// ...
}
然后你的控制器代码会看起来像
final int courseCode = Integer.valueOf(courseId.getText());
Task<Course> courseTask = new Task<Course>() {
@Override
public Course call() throws Exception {
return myDAO.getCourseByCode(courseCode);
}
};
courseTask.setOnSucceeded(e -> {
Course course = courseTask.getCourse();
if (course != null) {
courseCodeLbl.setText(course.getName());
}
});
exec.execute(courseTask);
Task
的 API 文档还有更多示例,包括更新任务的属性(对进度条有用...等)。progress
相关