使用线程发出数据库请求

使用线程发出数据库请求,第1张

使用线程发出数据库请求

JavaFX的线程规则

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

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

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

IllegalStateException
您看到的原因:您是
courseCodeLbl.setText(...)
从FX
Application Thread以外的线程进行调用。


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

因此,如果您有一个长时间运行的 *** 作(例如访问数据库),需要在完成时更新UI,则基本计划是在后台线程中执行长时间运行的 *** 作,并在后台线程返回 *** 作的结果。完成,然后在UI(FX应用程序)线程上安排对UI的更新。所有单线程UI工具包都有执行此 *** 作的机制:在JavaFX中,您可以通过在FX
Application Thread上

Platform.runLater(Runnabler)
执行调用来实现
r.run()
。(在Swing中,您可以调用在AWT事件分配线程上
SwingUtilities.invokeLater(Runnabler)
执行
r.run()
。)JavaFX(请参见本答案后面的内容)还提供了一些更高级别的API,用于管理与FX Application
Thread的通信。

多线程通用通用做法

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

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

使用javafx.concurrent API

JavaFX提供了一个并发API,该API旨在在后台线程中执行代码,而该API专门用于在代码执行完成时(或执行期间)更新JavaFX
UI。此API旨在与

java.util.concurrent
API进行交互,该API提供了用于编写多线程代码的常规工具(但没有UI挂钩)。中的键类
javafx.concurrent
Task
,它表示打算在后台线程上执行的单个一次性工作单元。此类定义了一个抽象方法,
call()
该方法不带任何参数,返回结果并可能引发检查的异常。用调用的方法
Task
实现。
Runnable``run()``call()``Task
还具有被保证更新状态的FX应用程序线程的方法,例如作为一个集合
updateProgress(...)
updateMessage(...)
等,这定义了一些可观察到的性质(例如
state
value
):听众对这些属性将被通知的FX应用程序线程的变化。最后,有一些方便的方法来注册处理程序(
setonSucceeded(...)
setonFailed(...)
等); 通过这些方法注册的所有处理程序也将在FX Application Thread上调用。

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

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

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

因此,您可能会有一个这样的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)。
widgetTable.setItems(...)
实际上,使用
Task
的便捷回调方法
setonSucceeded(...)
(满足规则1)在FX Application线程上执行了对UI()的更新。

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

public class MyDAO {    private Connection conn ;    // constructor etc...    public Course getCourseByCode(int pre) throws SQLException {        try (PreparedStatement pstmt = conn.prepareStatement("select * from course where c_pre = ?")) { pstmt.setInt(1, pre); 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 pre 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文档

Task
有更多的例子,包括更新
progress
任务的属性进度条…等(有用



欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/4911553.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-11-12
下一篇 2022-11-12

发表评论

登录后才能评论

评论列表(0条)

保存