【问题标题】:Using threads to make database requests使用线程发出数据库请求
【发布时间】:2020-10-06 00:19:21
【问题描述】:

我试图了解线程在 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();
        }
    }

}

这会返回一个异常:

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

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

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

以前从未使用过线程,但我认为是时候使用它了。

【问题讨论】:

  • 当您看起来需要当前线程中的结果时,为什么要在第二个线程中运行查询?
  • @DavidEhrmann 通常,访问数据库是一个长时间运行的过程,因此不应在 FX 应用程序线程上完成(因为它会使 UI 无响应)。所以 OP 的直觉在这里是正确的:在另一个线程上执行长时间运行的进程:(s)他只需要在 FX 应用程序线程上安排对结果的处理。
  • 助记符:我不太明白您的实际示例应该做什么。您从数据库中读取了一堆行,并为每一行设置一个(单个、固定)标签的文本。最终结果(如果您修复线程错误)是标签将仅显示从数据库读取的最后一行的值;所有其他行都被丢弃。我为您的问题提供了一个高级别的答案(我认为这实际上是您似乎正在寻找的),但我故意没有参考您的示例,因为我真的不知道它应该做什么。
  • 查询是这样的:“select c_name from course where c_code = 1”。因为 c_code 在表中是 pk 它只返回一行。没什么先进的。我确实理解您的困惑,我使用了“while”而不是“if”,我为这种困惑道歉。我将编辑我的问题。我会在一分钟内浏览你的帖子。谢谢你帮助我。
  • 啊,好吧,那确实有道理。更新了答案以包含该特定案例。

标签: java mysql multithreading javafx


【解决方案1】:

这与数据库无关。 JavaFx 与几乎所有 GUI 库一样,要求您仅使用主 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

【讨论】:

    【解决方案2】:

    JavaFX 的线程规则

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

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

    第一个规则的原因是,与大多数 UI 工具包一样,框架的编写没有对场景图元素的状态进行任何同步。添加同步会产生性能成本,而这对于 UI 工具包来说是一个令人望而却步的成本。因此只有一个线程可以安全地访问这个状态。由于 UI 线程(JavaFX 的 FX 应用程序线程)需要访问此状态来渲染场景,因此 FX 应用程序线程是您可以访问“实时”场景图状态的唯一线程。在 JavaFX 8 及更高版本中,受此规则约束的大多数方法都会执行检查并在违反规则时抛出运行时异常。 (这与 Swing 形成对比,您可以在其中编写“非法”代码,并且它可能看起来运行良好,但实际上很容易在任意时间出现随机和不可预测的故障。)这是导致您看到的IllegalStateException:您正在从 FX 应用程序线程以外的线程调用 courseCodeLbl.setText(...)

    第二条规则的原因是FX Application Thread,除了负责处理用户事件之外,还负责渲染场景。因此,如果您在该线程上执行长时间运行的操作,则在该操作完成之前不会呈现 UI,并且将对用户事件无响应。虽然这不会产生异常或导致损坏的对象状态(违反规则 1 会),但它(充其量)会造成糟糕的用户体验。

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

    多线程的一般良好实践

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

    在某些情况下,需要在多个线程之间共享可变状态。当这绝对必须完成时,您需要仔细同步对该状态的访问以避免观察到不一致状态的状态(还有其他更微妙的问题需要解决,例如状态的活跃性等)。强烈建议在需要时使用高级库来为您管理这些复杂性。

    使用 javafx.concurrent API

    JavaFX 提供了一个concurrency API,该concurrency API 设计用于在后台线程中执行代码,其 API 专门用于在代码执行完成时(或期间)更新 JavaFX UI。此 API 旨在与 java.util.concurrent API 交互,后者提供了编写多线程代码的通用工具(但没有 UI 挂钩)。 javafx.concurrent 中的关键类是Task,它代表一个单一的、一次性的、旨在在后台线程上执行的工作单元。这个类定义了一个抽象方法call(),它不接受任何参数,返回一个结果,并且可能抛出检查异常。 Task 使用其 run() 方法实现 Runnable,只需调用 call()Task 也有一组保证更新 FX 应用线程状态的方法,例如 updateProgress(...)updateMessage(...) 等。它定义了一些可观察的属性(例如 statevalue): listeners FX 应用程序线程上的更改将通知这些属性。最后,还有一些注册处理程序的便捷方法(setOnSucceeded(...)setOnFailed(...) 等);通过这些方法注册的任何处理程序也将在 FX 应用程序线程上调用。

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

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

    对于数据库访问,我强烈建议将实际的数据库代码封装在一个对 UI 一无所知的单独类中 (Data Access Object design pattern)。然后让任务调用数据访问对象上的方法。

    所以你可能有一个像这样的 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 中的,该 Task 在后台线程上运行(通过访问器)以防止阻塞 UI(上面的规则 2)。 UI (widgetTable.setItems(...)) 的更新实际上是在 FX 应用程序线程上执行的,使用 Task 的便捷回调方法 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);
    

    API docs for Task 有更多示例,包括更新任务的progress 属性(对进度条很有用...等。

    【讨论】:

      【解决方案3】:

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

      异常试图告诉您您正在尝试访问 JavaFX 应用程序线程之外的 JavaFX 场景图。但是在哪里??

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

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

      导致相似解决方案的不同方法。

      用 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();
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-08-06
        相关资源
        最近更新 更多