使用线程发出数据库请求

时间:2015-05-15 00:11:09

标签: java mysql multithreading javafx

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

}

这会返回一个异常:

  

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

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

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

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

3 个答案:

答案 0 :(得分:44)

JavaFX的线程规则

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

  1. 修改或访问作为场景图一部分的节点状态的任何代码必须在JavaFX应用程序线程上执行。某些其他操作(例如,创建新的Stage)也受此规则的约束。
  2. 任何可能需要很长时间才能运行的代码应在后台线程上执行(即不在FX应用程序主题上)。
  3. 第一条规则的原因在于,与大多数UI工具包一样,框架的编写没有任何与场景图元素状态的同步。添加同步会产生性能成本,这对UI工具包来说是一个令人望而却步的成本。因此,只有一个线程可以安全地访问此状态。由于UI线程(用于JavaFX的FX应用程序线程)需要访问此状态以呈现场景,因此FX应用程序线程是您可以访问&#34; live&#34;场景图状态。在JavaFX 8及更高版本中,受此规则约束的大多数方法都会执行检查并在违反规则时抛出运行时异常。 (这与Swing形成鲜明对比,在Swing中,您可以编写&#34;非法&#34;代码,它可能看起来运行正常,但实际上在任意时间容易出现随机和不可预测的故障。) 这是IllegalStateException您看到的 的原因:您从FX应用程序主题以外的线程调用courseCodeLbl.setText(...)

    第二条规则的原因是FX应用程序线程以及负责处理用户事件,也负责渲染场景。因此,如果在该线程上执行长时间运行的操作,则在该操作完成之前不会呈现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的通信应用程序线程。

    多线程的一般良好实践

    使用多个线程的最佳实践是构造要在&#34;用户定义的&#34;上执行的代码。线程作为以某种固定状态初始化的对象,具有执行操作的方法,并且在完成时返回表示结果的对象。使用不可变对象用于初始化状态和计算结果是非常期望的。这里的想法是消除尽可能从多个线程可见任何可变状态的可能性。从数据库访问数据非常符合这个习惯用法:您可以初始化您的工作人员&#34;具有数据库访问参数的对象(搜索术语等)。执行数据库查询并获取结果集,使用结果集填充域对象集合,并在结尾处返回集合。

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

    使用javafx.concurrent API

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

    因此,从数据库中检索数据的通用公式是:

    1. 创建Task来处理对数据库的调用。
    2. 使用执行数据库调用所需的任何状态初始化Task
    3. 实施任务的call()方法以执行数据库调用,返回调用结果。
    4. 使用任务注册处理程序,以便在完成后将结果发送到UI。
    5. 在后台线程上调用任务。
    6. 对于数据库访问,我强烈建议将实际数据库代码封装在一个对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中,以防止阻塞UI(上面的规则2)。 UI(widgetTable.setItems(...))的更新实际上使用Task的便捷回调方法setOnSucceeded(...)在FX应用程序线程上执行(满足规则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属性(对进度条有用......等等。

答案 1 :(得分:8)

  

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

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

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

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

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

使用Platform.runLater

包裹场景图元素

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

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

答案 2 :(得分:3)

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