使用线程来发出数据库请求

我想了解如何线程在java中工作。 这是一个返回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(); } } } 

这会返回一个exception:

线程“线程A”中的exceptionjava.lang.IllegalStateException:不在FX应用程序线程上; currentThread =线程A

如何正确地实现线程,以便每个数据库请求在第二个线程而不是主线程中执行?

我听说过实现Runnable,但是如何在run方法中调用不同的方法?

以前从来没有使用线程,但我认为是时候了。

JavaFX的线程规则

线程和JavaFX有两个基本规则:

  1. 任何修改或访问属于场景图一部分的节点状态的代码都必须在JavaFX应用程序线程上执行。 某些其他操作(例如创build新的Stage )也受此规则约束。
  2. 任何可能需要很长时间运行的代码都应该在后台线程上执行(即不在FX应用程序线程中)。

第一条规则的原因是,像大多数UI工具包一样,框架的编写没有任何同步场景图元素的状态。 增加同步会导致性能成本,而这对于UI工具包来说是一个令人望而却步的成本。 因此只有一个线程可以安全地访问这个状态。 由于UI线程(用于JavaFX的FX应用程序线程)需要访问这个状态来渲染场景,因此FX应用程序线程是唯一可以访问“活动”场景图状态的线程。 在JavaFX 8和更高版本中,受规则约束的大多数方法都执行检查并抛出运行时exception。 (这与Swing相反,在那里你可以编写“非法”的代码,它可能看起来运行良好,但是实际上在任意时间都会出现随机和不可预知的错误。) 这就是你所看到的IllegalStateException的原因 :您从FX应用程序线程以外的线程调用courseCodeLbl.setText(...)

第二条规则的原因是外汇应用程序线程,以及负责处理用户事件,也负责渲染场景。 因此,如果您在该线程上执行了长时间运行的操作,那么在该操作完成之前,UI将不会呈现,并且将对用户事件无响应。 虽然这不会产生exception或导致对象状态被破坏(违反了规则1),但它(最多)会造成糟糕的用户体验。

因此,如果您有一个需要在完成时更新UI的长时间运行的操作(如访问数据库),基本计划是在后台线程中执行长时间运行的操作,并返回操作结果完成,然后在UI(FX应用程序)线程上安排对UI的更新。 所有单线程UI工具包都有这样的机制:在JavaFX中,您可以通过调用Platform.runLater(Runnable r)在FX应用程序线程上执行r.run()来实现。 (在Swing中,可以调用SwingUtilities.invokeLater(Runnable r)在AWT事件派发线程上执行r.run() 。JavaFX(参见本答复后面的内容)还提供了一些更高级别的API来pipe理通信FX应用程序线程。

multithreading的一般良好实践

使用multithreading的最佳做法是将要在“用户定义”线程上执行的代码构造为一个以某种固定状态初始化的对象,具有执行该操作的方法,并在完成时返回一个对象代表结果。 使用不可变对象作为初始化状态和计算结果是非常理想的。 这里的想法是尽可能消除从多个线程可见的任何可变状态的可能性。 从数据库访问数据很好地符合这个习惯用法:你可以使用数据库访问参数(search条件等)初始化你的“worker”对象。 执行数据库查询并获取结果集,使用结果集来填充域对象的集合,并在最后返回集合。

在某些情况下,有必要在多个线程之间共享可变状态。 当这一切必须完成的时候,你需要仔细地同步访问该状态,以避免观察状态处于不一致状态(还有其他更加微妙的问题需要处理,比如状态的活跃等)。 当需要的时候强烈build议使用高级库来为你pipe理这些复杂性。

使用javafx.concurrent API

JavaFX提供了一个并发API ,专为在后台线程中执行代码而devise,其API专门用于在代码执行完成(或执行期间)时更新JavaFX UI。 此API旨在与java.util.concurrent API进行交互,该API提供了编写multithreading代码的一般function(但没有UI挂钩)。 javafx.concurrent的关键类是Task ,它表示一个单一的,一次性的工作单元,打算在后台线程上执行。 这个类定义了一个抽象方法call() ,它不接受任何参数,返回一个结果,并且可以抛出检查的exception。 Task使用其run()方法简单地调用call()实现RunnableTask还有一组保证更新FX应用程序线程状态的方法,比如updateProgress(...)updateMessage(...)等。它定义了一些可观察的属性(例如statevalue ):侦听器这些属性将被通知FX应用程序线程的变化。 最后,有一些便利的方法来注册处理程序( setOnSucceeded(...)setOnFailed(...)等); 任何通过这些方法注册的处理程序也将在FX应用程序线程中调用。

所以从数据库中检索数据的一般公式是:

  1. 创build一个Task来处理对数据库的调用。
  2. 使用执行数据库调用所需的任何状态初始化Task
  3. 实现任务的call()方法来执行数据库调用,返callback用的结果。
  4. 注册一个带有任务的处理程序,在完成时将结果发送到UI。
  5. 在后台线程上调用该任务。

对于数据库访问,我强烈build议将实际的数据库代码封装在一个对UI( 数据访问对象devise模式 )一无所知的单独的类中。 然后只需要调用数据访问对象上的方法。

所以你可能有这样的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方法的调用是如何封装在后台线程(通过访问器)运行的Task ,以防止阻塞UI(上述规则2)。 UI( widgetTable.setItems(...) )的更新实际上是在FX应用程序线程上执行的,使用Task的方便callback方法setOnSucceeded(...) (满足规则1)。

在你的情况下,你正在执行的数据库访问返回一个单一的结果,所以你可能有一个类似的方法

 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属性(对进度条等有用)。

线程“线程A”中的exceptionjava.lang.IllegalStateException:不在FX应用程序线程上; currentThread =线程A

例外是试图告诉你,你正试图访问JavaFX应用程序线程之外的JavaFX场景图。 但是哪里 ??

 courseCodeLbl.setText(rs.getString(1)); // <--- The culprit 

如果我不能这样做,我该如何使用后台线程?

这是不同的方法,导致类似的解决scheme。

用Platform.runLater包装你的场景图元素

最简单和最简单的方法是将上面的代码行包装在Plaform.runLater ,以便在JavaFX Application线程上执行。

 Platform.runLater(() -> courseCodeLbl.setText(rs.getString(1))); 

使用任务

使用这些scheme的更好的方法是使用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(); } } 

这与数据库无关。 像几乎所有的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(); } 

或者,您可以使用任务 。