首页 文章

使用线程发出数据库请求

提问于
浏览
17

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

}

这会返回一个异常:

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

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

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

从来没有使用过线程,但我认为是时候了 .

3 回答

  • 1

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

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

    Threading Rules for JavaFX

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

    • 在JavaFX应用程序线程上执行任何修改或访问作为场景图的一部分的节点状态的代码 must . 某些其他操作(例如,创建新的 Stage )也受此规则的约束 .

    • 任何可能需要很长时间才能运行的代码 should 可以在后台线程上执行(即不在FX应用程序线程上执行) .

    第一个规则的原因是,与大多数UI工具包一样,框架的编写没有任何与场景图元素状态的同步 . 添加同步会产生性能成本,这对UI工具包来说是一个令人望而却步的成本 . 因此,只有一个线程可以安全地访问此状态 . 由于UI线程(用于JavaFX的FX应用程序线程)需要访问此状态以呈现场景,因此FX应用程序线程是您可以访问"live"场景图状态的唯一线程 . 在JavaFX 8及更高版本中,如果违反规则,大多数受此规则约束的方法都会执行检查并抛出运行时异常 . (这与Swing相反,你可以在其中编写"illegal"代码,它可能看起来运行正常,但实际上在任意时间都容易出现随机和不可预测的故障 . ) This is the cause of the IllegalStateException you are seeing :你从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应用程序线程的通信 .

    General Good Practices for Multithreading

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

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

    Using the javafx.concurrent API

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

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

    • 创建 Task 以处理对数据库的调用 .

    • 使用执行数据库调用所需的任何状态初始化 Task .

    • 实现任务的 call() 方法以执行数据库调用,返回调用的结果 .

    • 使用任务注册处理程序,以便在完成后将结果发送到UI .

    • 在后台线程上调用任务 .

    对于数据库访问,我强烈建议将实际数据库代码封装在一个对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(...) )的更新实际上是使用 Task 的便捷回调方法setOnSucceeded(...)(满足规则1)在FX应用程序线程上执行的 .

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

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

相关问题