java - 使用线程发出数据库请求

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

}


这将返回一个异常:


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


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

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

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

最佳答案

JavaFX的线程规则

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


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


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

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

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

多线程通用良好实践

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

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

使用javafx.concurrent API

JavaFX提供了一个concurrency API,旨在用于在后台线程中执行代码,而API专门用于在代码执行完成时(或执行期间)更新JavaFX UI。此API旨在与java.util.concurrent API进行交互,该javafx.concurrent提供了用于编写多线程代码的常规工具(但没有UI挂钩)。 Task中的键类是call(),它表示打算在后台线程上执行的单个一次性工作单元。此类定义单个抽象方法Task,该方法不带任何参数,返回结果并可能引发检查的异常。 Runnable只需调用run()即可通过其call()方法实现TaskupdateProgress(...)还具有保证可以更新FX Application Thread上状态的方法的集合,例如updateMessage(...)state等。它定义了一些可观察的属性(例如valuesetOnSucceeded(...)):侦听器这些属性的设置将在FX Application Thread上得到通知。最后,有一些方便的方法可以注册处理程序(setOnFailed(...)Data Access Object design pattern等);通过这些方法注册的所有处理程序也将在FX Application Thread上调用。

因此,从数据库检索数据的一般公式为:


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


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

因此,您可能会有一个这样的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方法的调用如何包装在widgetTable.setItems(...)中,该Task在后台线程上运行(通过访问器),以防止阻塞UI(上面的规则2)。实际上,使用setOnSucceeded(...)的便捷回调方法API docs for Task(满足规则1)在FX Application线程上执行对UI(progress)的更新。

在您的情况下,您正在执行的数据库访问将返回单个结果,因此您可能具有类似以下的方法

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


有更多示例,包括更新任务的属性(对进度条有用...等)。

关于java - 使用线程发出数据库请求,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30292322/

相关文章:

mysql - 如果这样记录得到的值如何取小于0的值?

mysql - SQL计算特定范围内的行数

iOS:在单独的线程中加载图像时保持用户界面响应

python - 在线程中更改 QObject 样式表时出错

java - 在实例化列表的变量中使用时,主列表值会发生变化

java - 如何避免 Set<Object> 中的重复

mysql - 使用mysql如何在字符串中找到字母表的第一次出现

c - 多个线程访问一个变量

java - 如何拆分多行 INSERT INTO sql 语句?

java - 验证 JTextField 中数据的最佳方法是什么?