使用线程进行数据库请求
Using threads to make database requests
我正在尝试了解线程在 java 中的工作原理。这是一个简单的数据库请求 returns 一个 ResultSet。我正在使用 JavaFx。
package application;
import java.sql.ResultSet;
import java.sql.SQLException;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
public class Controller{
@FXML
private Button getCourseBtn;
@FXML
private TextField courseId;
@FXML
private Label courseCodeLbl;
private ModelController mController;
private void requestCourseName(){
String courseName = "";
Course c = new Course();
c.setCCode(Integer.valueOf(courseId.getText()));
mController = new ModelController(c);
try {
ResultSet rs = mController.<Course>get();
if(rs.next()){
courseCodeLbl.setText(rs.getString(1));
}
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// return courseName;
}
public void getCourseNameOnClick(){
try {
// courseCodeLbl.setText(requestCourseName());
Thread t = new Thread(new Runnable(){
public void run(){
requestCourseName();
}
}, "Thread A");
t.start();
} catch (NumberFormatException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
这 returns 一个例外:
Exception in thread "Thread A" java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread A
如何正确实现线程,以便每个数据库请求都在第二个线程而不是主线程中执行?
我听说过实现 Runnable 但我如何在 运行 方法中调用不同的方法?
以前从未使用过线程,但我认为是时候了。
这与数据库无关。与几乎所有 GUI 库一样,JavaFx 要求您仅使用主 UI 线程来修改 GUI.
您需要将数据从数据库传回主线程UI。使用 Platform.runLater() 将 Runnable 安排为主 UI 线程中的 运行。
public void getCourseNameOnClick(){
new Thread(new Runnable(){
public void run(){
String courseName = requestCourseName();
Platform.runLater(new Runnable(){
courseCodeLbl.setText(courseName)
});
}
}, "Thread A").start();
}
或者,您可以 use Task。
JavaFX 线程规则
线程和 JavaFX 有两个基本规则:
- 任何修改或访问作为场景图一部分的节点状态的代码 必须 在 JavaFX 应用程序线程上执行。某些其他操作(例如创建新的
Stage
s)也受此规则约束。
- 任何可能需要很长时间才能 运行 的代码都应该 在后台线程上执行(即不在 FX 应用程序线程上)。
第一条规则的原因是,与大多数 UI 工具包一样,编写框架时没有对场景图元素的状态进行任何同步。添加同步会产生性能成本,而这对于 UI 工具包来说是一个令人望而却步的成本。因此只有一个线程可以安全地访问这一状态。由于 UI 线程(JavaFX 的 FX 应用程序线程)需要访问此状态以渲染场景,因此 FX 应用程序线程是唯一可以访问 "live" 场景图状态的线程。在 JavaFX 8 及更高版本中,大多数受此规则约束的方法都会执行检查并在违反规则时抛出 运行time 异常。 (这与 Swing 形成对比,您可以在其中编写 "illegal" 代码,它可能看起来 运行 很好,但实际上在任意时间容易出现随机且不可预测的故障。) 这就是您所看到的 IllegalStateException
的原因:您正在从 FX 应用程序线程以外的线程调用 courseCodeLbl.setText(...)
。
第二条规则的原因是FX应用线程除了负责处理用户事件外,还负责渲染场景。因此,如果您在该线程上执行长 运行ning 操作,则 UI 将在该操作完成之前不会呈现,并且将变得对用户事件无响应。虽然这不会产生异常或导致损坏的对象状态(如违反规则 1 那样),但它(充其量)会造成糟糕的用户体验。
因此如果你有一个 long-运行ning 操作(例如访问数据库)需要在完成时更新 UI,基本计划是执行 long-运行在后台线程中进行操作,return在操作完成后获取操作结果,然后在 UI(FX 应用程序)线程上安排对 UI 的更新.所有单线程 UI 工具包都有执行此操作的机制:在 JavaFX 中,您可以通过调用 Platform.runLater(Runnable r)
在 FX 应用程序线程上执行 r.run()
来执行此操作。 (在 Swing 中,您可以调用 SwingUtilities.invokeLater(Runnable r)
以在 AWT 事件调度线程上执行 r.run()
。)JavaFX(请参阅本答案后面部分)还提供了一些更高级别的 API 来管理通信回到 FX 应用线程。
多线程的一般良好做法
使用多线程的最佳实践是将要在 "user-defined" 线程上执行的代码构造为一个对象,该对象以某种固定状态初始化,具有执行操作的方法,并且完成后 returns 一个表示结果的对象。对初始化状态和计算结果使用不可变对象是非常可取的。这里的想法是尽可能消除从多个线程可见的任何可变状态的可能性。从数据库访问数据非常适合这个习惯用法:您可以使用数据库访问参数(搜索词等)初始化 "worker" 对象。执行数据库查询并得到结果集,使用结果集填充域对象的集合,最后return集合。
在某些情况下,有必要在多个线程之间共享可变状态。当绝对必须这样做时,您需要小心地同步对该状态的访问,以避免观察到处于不一致状态的状态(还有其他更微妙的问题需要解决,例如状态的活跃度等)。强烈建议在需要时使用高级库来为您管理这些复杂性。
使用 javafx.concurrent API
JavaFX 提供了 concurrency API that is designed for executing code in a background thread, with API specifically designed for updating the JavaFX UI on completion of (or during) the execution of that code. This API is designed to interact with the java.util.concurrent
API, which provides general facilities for writing multithreaded code (but with no UI hooks). The key class in javafx.concurrent
is Task
, which represents a single, one-off, unit of work intended to be performed on a background thread. This class defines a single abstract method, call()
, which takes no parameters, returns a result, and may throw checked exceptions. Task
implements Runnable
with its run()
method simply invoking call()
. Task
also has a collection of methods which are guaranteed to update state on the FX Application Thread, such as updateProgress(...)
, updateMessage(...)
, etc. It defines some observable properties (e.g. state
and value
): listeners to these properties will be notified of changes on the FX Application Thread. Finally, there are some convenience methods to register handlers (setOnSucceeded(...)
, setOnFailed(...)
,等等);通过这些方法注册的任何处理程序也将在 FX 应用程序线程上调用。
所以从数据库中检索数据的通用公式是:
- 创建一个
Task
来处理对数据库的调用。
- 使用执行数据库调用所需的任何状态初始化
Task
。
- 执行任务的
call()
方法来执行数据库调用,return获取调用的结果。
- 注册一个处理程序,完成后将结果发送到 UI。
- 在后台线程上调用任务。
对于数据库访问,我强烈建议将实际的数据库代码封装在一个单独的 class 中,它对 UI (Data Access Object design pattern) 一无所知。然后让任务调用数据访问对象上的方法。
所以你可能有一个像这样的 DAO class(注意这里没有 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 class(例如控制器 class)的任何调用都应将其安排在后台线程上。控制器 class 可能如下所示:
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);
}
// ...
}
注意对(可能)长运行ning DAO 方法的调用是如何包装在 Task
中的,它是 运行 在后台线程上(通过访问器)以防止阻塞 UI(上面的规则 2)。 UI (widgetTable.setItems(...)
) 的更新实际上是在 FX 应用程序线程上执行的,使用 Task
的便捷回调方法 setOnSucceeded(...)
(满足规则 1)。
在您的情况下,您正在执行的数据库访问 return 是单个结果,因此您可能有类似
的方法
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);
API docs for Task
还有很多例子,包括更新任务的progress
属性(对进度条很有用...等
Exception in thread "Thread A" java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread A
该异常试图告诉您您正试图在 JavaFX 应用程序线程之外访问 JavaFX 场景图。但是在哪里??
courseCodeLbl.setText(rs.getString(1)); // <--- The culprit
If I can't do this how do I use a background thread?
导致相似解决方案的方法不同。
用Platform.runLater
包裹你的场景图元素
更简单和最简单的方法是将上面的行包装在 Plaform.runLater
中,这样它就可以在 JavaFX 应用程序线程上执行。
Platform.runLater(() -> courseCodeLbl.setText(rs.getString(1)));
使用任务
处理这些情况的更好的方法是使用Task,它有专门的方法来发回更新。在以下示例中,我使用 updateMessage
来更新消息。此 属性 绑定到 courseCodeLbl
textProperty。
Task<Void> task = new Task<Void>() {
@Override
public Void call() {
String courseName = "";
Course c = new Course();
c.setCCode(Integer.valueOf(courseId.getText()));
mController = new ModelController(c);
try {
ResultSet rs = mController.<Course>get();
if(rs.next()) {
// update message property
updateMessage(rs.getString(1));
}
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
}
public void getCourseNameOnClick(){
try {
Thread t = new Thread(task);
// To update the label
courseCodeLbl.textProperty.bind(task.messageProperty());
t.setDaemon(true); // Imp! missing in your code
t.start();
} catch (NumberFormatException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
我正在尝试了解线程在 java 中的工作原理。这是一个简单的数据库请求 returns 一个 ResultSet。我正在使用 JavaFx。
package application;
import java.sql.ResultSet;
import java.sql.SQLException;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
public class Controller{
@FXML
private Button getCourseBtn;
@FXML
private TextField courseId;
@FXML
private Label courseCodeLbl;
private ModelController mController;
private void requestCourseName(){
String courseName = "";
Course c = new Course();
c.setCCode(Integer.valueOf(courseId.getText()));
mController = new ModelController(c);
try {
ResultSet rs = mController.<Course>get();
if(rs.next()){
courseCodeLbl.setText(rs.getString(1));
}
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// return courseName;
}
public void getCourseNameOnClick(){
try {
// courseCodeLbl.setText(requestCourseName());
Thread t = new Thread(new Runnable(){
public void run(){
requestCourseName();
}
}, "Thread A");
t.start();
} catch (NumberFormatException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
这 returns 一个例外:
Exception in thread "Thread A" java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread A
如何正确实现线程,以便每个数据库请求都在第二个线程而不是主线程中执行?
我听说过实现 Runnable 但我如何在 运行 方法中调用不同的方法?
以前从未使用过线程,但我认为是时候了。
这与数据库无关。与几乎所有 GUI 库一样,JavaFx 要求您仅使用主 UI 线程来修改 GUI.
您需要将数据从数据库传回主线程UI。使用 Platform.runLater() 将 Runnable 安排为主 UI 线程中的 运行。
public void getCourseNameOnClick(){
new Thread(new Runnable(){
public void run(){
String courseName = requestCourseName();
Platform.runLater(new Runnable(){
courseCodeLbl.setText(courseName)
});
}
}, "Thread A").start();
}
或者,您可以 use Task。
JavaFX 线程规则
线程和 JavaFX 有两个基本规则:
- 任何修改或访问作为场景图一部分的节点状态的代码 必须 在 JavaFX 应用程序线程上执行。某些其他操作(例如创建新的
Stage
s)也受此规则约束。 - 任何可能需要很长时间才能 运行 的代码都应该 在后台线程上执行(即不在 FX 应用程序线程上)。
第一条规则的原因是,与大多数 UI 工具包一样,编写框架时没有对场景图元素的状态进行任何同步。添加同步会产生性能成本,而这对于 UI 工具包来说是一个令人望而却步的成本。因此只有一个线程可以安全地访问这一状态。由于 UI 线程(JavaFX 的 FX 应用程序线程)需要访问此状态以渲染场景,因此 FX 应用程序线程是唯一可以访问 "live" 场景图状态的线程。在 JavaFX 8 及更高版本中,大多数受此规则约束的方法都会执行检查并在违反规则时抛出 运行time 异常。 (这与 Swing 形成对比,您可以在其中编写 "illegal" 代码,它可能看起来 运行 很好,但实际上在任意时间容易出现随机且不可预测的故障。) 这就是您所看到的 IllegalStateException
的原因:您正在从 FX 应用程序线程以外的线程调用 courseCodeLbl.setText(...)
。
第二条规则的原因是FX应用线程除了负责处理用户事件外,还负责渲染场景。因此,如果您在该线程上执行长 运行ning 操作,则 UI 将在该操作完成之前不会呈现,并且将变得对用户事件无响应。虽然这不会产生异常或导致损坏的对象状态(如违反规则 1 那样),但它(充其量)会造成糟糕的用户体验。
因此如果你有一个 long-运行ning 操作(例如访问数据库)需要在完成时更新 UI,基本计划是执行 long-运行在后台线程中进行操作,return在操作完成后获取操作结果,然后在 UI(FX 应用程序)线程上安排对 UI 的更新.所有单线程 UI 工具包都有执行此操作的机制:在 JavaFX 中,您可以通过调用 Platform.runLater(Runnable r)
在 FX 应用程序线程上执行 r.run()
来执行此操作。 (在 Swing 中,您可以调用 SwingUtilities.invokeLater(Runnable r)
以在 AWT 事件调度线程上执行 r.run()
。)JavaFX(请参阅本答案后面部分)还提供了一些更高级别的 API 来管理通信回到 FX 应用线程。
多线程的一般良好做法
使用多线程的最佳实践是将要在 "user-defined" 线程上执行的代码构造为一个对象,该对象以某种固定状态初始化,具有执行操作的方法,并且完成后 returns 一个表示结果的对象。对初始化状态和计算结果使用不可变对象是非常可取的。这里的想法是尽可能消除从多个线程可见的任何可变状态的可能性。从数据库访问数据非常适合这个习惯用法:您可以使用数据库访问参数(搜索词等)初始化 "worker" 对象。执行数据库查询并得到结果集,使用结果集填充域对象的集合,最后return集合。
在某些情况下,有必要在多个线程之间共享可变状态。当绝对必须这样做时,您需要小心地同步对该状态的访问,以避免观察到处于不一致状态的状态(还有其他更微妙的问题需要解决,例如状态的活跃度等)。强烈建议在需要时使用高级库来为您管理这些复杂性。
使用 javafx.concurrent API
JavaFX 提供了 concurrency API that is designed for executing code in a background thread, with API specifically designed for updating the JavaFX UI on completion of (or during) the execution of that code. This API is designed to interact with the java.util.concurrent
API, which provides general facilities for writing multithreaded code (but with no UI hooks). The key class in javafx.concurrent
is Task
, which represents a single, one-off, unit of work intended to be performed on a background thread. This class defines a single abstract method, call()
, which takes no parameters, returns a result, and may throw checked exceptions. Task
implements Runnable
with its run()
method simply invoking call()
. Task
also has a collection of methods which are guaranteed to update state on the FX Application Thread, such as updateProgress(...)
, updateMessage(...)
, etc. It defines some observable properties (e.g. state
and value
): listeners to these properties will be notified of changes on the FX Application Thread. Finally, there are some convenience methods to register handlers (setOnSucceeded(...)
, setOnFailed(...)
,等等);通过这些方法注册的任何处理程序也将在 FX 应用程序线程上调用。
所以从数据库中检索数据的通用公式是:
- 创建一个
Task
来处理对数据库的调用。 - 使用执行数据库调用所需的任何状态初始化
Task
。 - 执行任务的
call()
方法来执行数据库调用,return获取调用的结果。 - 注册一个处理程序,完成后将结果发送到 UI。
- 在后台线程上调用任务。
对于数据库访问,我强烈建议将实际的数据库代码封装在一个单独的 class 中,它对 UI (Data Access Object design pattern) 一无所知。然后让任务调用数据访问对象上的方法。
所以你可能有一个像这样的 DAO class(注意这里没有 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 class(例如控制器 class)的任何调用都应将其安排在后台线程上。控制器 class 可能如下所示:
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);
}
// ...
}
注意对(可能)长运行ning DAO 方法的调用是如何包装在 Task
中的,它是 运行 在后台线程上(通过访问器)以防止阻塞 UI(上面的规则 2)。 UI (widgetTable.setItems(...)
) 的更新实际上是在 FX 应用程序线程上执行的,使用 Task
的便捷回调方法 setOnSucceeded(...)
(满足规则 1)。
在您的情况下,您正在执行的数据库访问 return 是单个结果,因此您可能有类似
的方法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);
API docs for Task
还有很多例子,包括更新任务的progress
属性(对进度条很有用...等
Exception in thread "Thread A" java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread A
该异常试图告诉您您正试图在 JavaFX 应用程序线程之外访问 JavaFX 场景图。但是在哪里??
courseCodeLbl.setText(rs.getString(1)); // <--- The culprit
If I can't do this how do I use a background thread?
导致相似解决方案的方法不同。
用Platform.runLater
包裹你的场景图元素更简单和最简单的方法是将上面的行包装在 Plaform.runLater
中,这样它就可以在 JavaFX 应用程序线程上执行。
Platform.runLater(() -> courseCodeLbl.setText(rs.getString(1)));
使用任务
处理这些情况的更好的方法是使用Task,它有专门的方法来发回更新。在以下示例中,我使用 updateMessage
来更新消息。此 属性 绑定到 courseCodeLbl
textProperty。
Task<Void> task = new Task<Void>() {
@Override
public Void call() {
String courseName = "";
Course c = new Course();
c.setCCode(Integer.valueOf(courseId.getText()));
mController = new ModelController(c);
try {
ResultSet rs = mController.<Course>get();
if(rs.next()) {
// update message property
updateMessage(rs.getString(1));
}
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
}
public void getCourseNameOnClick(){
try {
Thread t = new Thread(task);
// To update the label
courseCodeLbl.textProperty.bind(task.messageProperty());
t.setDaemon(true); // Imp! missing in your code
t.start();
} catch (NumberFormatException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}