Ответ 1
Правила Threading для JavaFX
Существует два основных правила для потоков и JavaFX:
- Любой код, который изменяет или обращается к состоянию node, который является частью графика сцены должен, выполняться в потоке приложения JavaFX. Некоторые другие операции (например, создание новых
Stage
s) также связаны этим правилом. - Любой код, который может занять долгое время , должен выполняться в фоновом потоке (т.е. не в потоке приложения FX).
Причиной первого правила является то, что, как и большинство инструментов UI, структура записывается без какой-либо синхронизации состояния элементов графа сцены. Добавление синхронизации связано с затратами на производительность, и это оказывается непомерно высокой стоимостью для набора инструментов пользовательского интерфейса. Таким образом, только один поток может безопасно получить доступ к этому состоянию. Поскольку поток пользовательского интерфейса (поток прикладных программ для JavaFX) должен получить доступ к этому состоянию для рендеринга сцены, поток приложений FX - это единственный поток, на котором вы можете получить доступ к "живому" графику состояния сцены. В JavaFX 8 и более поздних версиях большинство методов, подчиненных этому правилу, выполняют проверки и исключают исключения выполнения во время выполнения, если правило нарушено. (Это контрастирует с Swing, где вы можете написать "незаконный" код, и может показаться, что он работает нормально, но на самом деле подвержен случайному и непредсказуемому сбою в произвольное время.) Это причина IllegalStateException
вы видите: вы вызываете courseCodeLbl.setText(...)
из потока, отличного от потока приложений FX.
Причиной второго правила является то, что поток приложений FX, а также ответственный за обработку пользовательских событий также отвечает за рендеринг сцены. Таким образом, если вы выполняете длительную операцию над этим потоком, пользовательский интерфейс не будет отображаться до завершения этой операции и перестанет отвечать на запросы пользователей. Хотя это не приведет к возникновению исключений или приведет к поврежденному состоянию объекта (как нарушает правило 1), он (в лучшем случае) создает плохой пользовательский интерфейс.
Таким образом, если у вас есть длительная работа (например, доступ к базе данных), которая должна обновить пользовательский интерфейс при завершении, основной план состоит в том, чтобы выполнить долговременную операцию в фоновом потоке, возвращая результаты операции когда он будет завершен, а затем запланировать обновление пользовательского интерфейса в потоке пользовательского интерфейса (FX Application). Все однопоточные инструменты пользовательского интерфейса имеют механизм для этого: в JavaFX вы можете сделать это, вызвав Platform.runLater(Runnable r)
для выполнения r.run()
в потоке приложения FX. (В Swing вы можете вызвать SwingUtilities.invokeLater(Runnable r)
для выполнения r.run()
в потоке отправки событий AWT.) JavaFX (см. Далее в этом ответе) также предоставляет некоторый API более высокого уровня для управления обращением к потоку приложения FX.
Общие рекомендации по многопоточности
Лучшей практикой работы с несколькими потоками является структурировать код, который должен выполняться в "определяемом пользователем" потоке, в качестве объекта, который инициализируется с некоторым фиксированным состоянием, имеет способ выполнения операции и завершения возвращает объект, представляющий результат. Желательно использовать неизменяемые объекты для инициализированного состояния и результата вычислений. Идея здесь заключается в том, чтобы исключить возможность того, что любое изменяемое состояние будет видно из нескольких потоков, насколько это возможно. Доступ к данным из базы данных отлично подходит для этой идиомы: вы можете инициализировать свой "рабочий" объект с параметрами доступа к базе данных (условия поиска и т.д.). Выполните запрос базы данных и получите набор результатов, используйте набор результатов для заполнения коллекции объектов домена и верните коллекцию в конце.
В некоторых случаях необходимо разделить изменяемое состояние между несколькими потоками. Когда это необходимо сделать, вам необходимо тщательно синхронизировать доступ к этому состоянию, чтобы избежать наблюдения за состоянием в несогласованном состоянии (есть и другие более тонкие проблемы, которые необходимо решить, такие как жизнеспособность состояния и т.д.). Сильная рекомендация, когда это необходимо, - использовать библиотеку высокого уровня для управления этими сложностями для вас.
Использование API javafx.concurrent
JavaFX предоставляет concurrency API, который предназначен для выполнения кода в фоновом потоке, с API, специально разработанным для обновления интерфейса JavaFX завершение (или во время) выполнения этого кода. Этот API предназначен для взаимодействия с java.util.concurrent
API, который предоставляет общие возможности для написания многопоточного кода (но без крючков UI). Ключевым классом в javafx.concurrent
является Task
, который представляет собой единую единовременную единицу работы, предназначенную для выполнения на фоне нить. Этот класс определяет один абстрактный метод call()
, который не принимает никаких параметров, возвращает результат и может выдавать проверенные исключения. Task
реализует Runnable
с помощью метода run()
, просто вызывающего call()
. Task
также имеет набор методов, которые гарантируют обновление состояния в потоке приложения FX, например updateProgress(...)
, updateMessage(...)
и т.д. Он определяет некоторые наблюдаемые свойства (например, state
и value
): слушатели этих свойств будут уведомлены об изменениях в потоке приложения FX. Наконец, существуют некоторые удобные методы регистрации обработчиков (setOnSucceeded(...)
, setOnFailed(...)
и т.д.); любые обработчики, зарегистрированные с помощью этих методов, также будут вызываться в потоке приложения FX.
Таким образом, общая формула для извлечения данных из базы данных:
- Создайте
Task
для обработки вызова в базу данных. - Инициализировать
Task
с любым состоянием, необходимым для выполнения вызова базы данных. - Реализовать метод
call()
задачи для выполнения вызова базы данных, возвращая результаты вызова. - Зарегистрируйте обработчик с задачей отправить результаты в пользовательский интерфейс, когда он будет завершен.
- Вызов задачи в фоновом потоке.
Для доступа к базе данных я настоятельно рекомендую инкапсулировать фактический код базы данных в отдельный класс, который ничего не знает об интерфейсе пользовательского интерфейса (Data Access Object). Затем просто попросите задачу вызвать методы на объекте доступа к данным.
Итак, у вас может быть класс DAO (обратите внимание, что здесь нет кода интерфейса):
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
, который запускается в фоновом потоке (через аксессор), чтобы предотвратить блокировку пользовательского интерфейса (правило 2 выше). Обновление пользовательского интерфейса (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 для Task
содержит много других примеров, включая обновление свойства progress
задачи (полезно для индикаторов выполнения)... и т.д.