Подтвердить что ты не робот

Использование потоков для создания запросов к базе данных

Я пытаюсь понять, как потоки работают в 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 = Thread A

Как правильно выполнить потоки, чтобы каждый запрос базы данных выполнялся во втором потоке вместо основного потока?

Я слышал о внедрении Runnable, но как я могу вызвать разные методы в методе run?

Никогда раньше не работал с потоками, но я думал, что на это настало время.

4b9b3361

Ответ 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 задачи (полезно для индикаторов выполнения)... и т.д.

Ответ 2

Исключение в потоке "Thread A" java.lang.IllegalStateException: не в потоке приложения FX; currentThread = Thread A

Исключение пытается сказать вам, что вы пытаетесь получить доступ к графику сцены JavaFX вне потока приложений JavaFX. Но где?

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

Если я не могу это сделать, как использовать фоновый поток?

Существуют разные подходы, которые приводят к аналогичным решениям.

Оберните элемент графика сцены с помощью Platform.runLater

Там проще и проще всего обернуть вышеприведенную строку в Plaform.runLater, чтобы она выполнялась в потоке приложений JavaFX.

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

Ответ 3

Это не имеет ничего общего с базой данных. JavaFx, как и почти все библиотеки GUI, требует, чтобы вы использовали только основной поток пользовательского интерфейса для изменения GUI.

Вам необходимо передать данные из базы данных обратно в основной поток пользовательского интерфейса. Используйте Platform.runLater(), чтобы запланировать запуск Runnable в основном потоке пользовательского интерфейса.

public void getCourseNameOnClick(){
    new Thread(new Runnable(){
        public void run(){
            String courseName = requestCourseName();
            Platform.runLater(new Runnable(){
                courseCodeLbl.setText(courseName)
            });
        }
    }, "Thread A").start();
}

В качестве альтернативы вы можете использовать задачу.