web-dev-qa-db-fra.com

Utilisation de threads pour effectuer des demandes de base de données

J'essaie de comprendre comment les threads fonctionnent en Java. Il s'agit d'une simple demande de base de données qui renvoie un ResultSet. J'utilise 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();
        }
    }

}

Cela renvoie une exception:

Exception dans le thread "Thread A" Java.lang.IllegalStateException: Pas sur le thread d'application FX; currentThread = Thread A

Comment est-ce que j'implémente correctement le threading pour que chaque requête de base de données soit exécutée dans un deuxième thread au lieu du thread principal?

J'ai entendu parler de l'implémentation de Runnable mais comment puis-je invoquer différentes méthodes dans la méthode d'exécution?

Je n'ai jamais travaillé avec le filetage auparavant, mais je pensais que c'était le moment.

22
Mnemonics

Règles de thread pour JavaFX

Il existe deux règles de base pour les threads et JavaFX:

  1. Tout code qui modifie ou accède à l'état d'un nœud faisant partie d'un graphe de scène doit être exécuté sur le thread d'application JavaFX. Certaines autres opérations (par exemple la création de nouveaux Stages) sont également liées par cette règle.
  2. Tout code dont l'exécution peut prendre du temps doit être exécuté sur un thread d'arrière-plan (c'est-à-dire pas sur le thread d'application FX).

La raison de la première règle est que, comme la plupart des boîtes à outils d'interface utilisateur, le cadre est écrit sans aucune synchronisation sur l'état des éléments du graphe de scène. L'ajout de la synchronisation entraîne un coût de performance, et cela s'avère être un coût prohibitif pour les boîtes à outils d'interface utilisateur. Ainsi, un seul thread peut accéder en toute sécurité à cet état. Étant donné que le thread d'interface utilisateur (FX Application Thread pour JavaFX) doit accéder à cet état pour rendre la scène, le thread d'application FX est le seul thread sur lequel vous pouvez accéder à l'état du graphique de scène "en direct". Dans JavaFX 8 et versions ultérieures, la plupart des méthodes soumises à cette règle effectuent des vérifications et lèvent des exceptions d'exécution si la règle est violée. (Cela contraste avec Swing, où vous pouvez écrire du code "illégal" et il peut sembler fonctionner correctement, mais est en fait sujet à des échecs aléatoires et imprévisibles à un moment arbitraire.) Ceci est la cause du IllegalStateException que vous voyez: vous appelez courseCodeLbl.setText(...) à partir d'un thread autre que l'application FX Fil.

La raison de la deuxième règle est que le thread d'application FX, en plus d'être responsable du traitement des événements utilisateur, est également responsable du rendu de la scène. Ainsi, si vous effectuez une opération de longue durée sur ce thread, l'interface utilisateur ne sera pas rendue tant que cette opération n'est pas terminée et ne répondra plus aux événements utilisateur. Bien que cela ne génère pas d'exceptions ou ne provoque pas d'état d'objet corrompu (comme le violera la règle 1), cela crée (au mieux) une mauvaise expérience utilisateur.

Ainsi, si vous avez une opération de longue durée (comme l'accès à une base de données) qui doit mettre à jour l'interface utilisateur à la fin, le plan de base consiste à effectuer l'opération de longue durée dans un thread d'arrière-plan, en renvoyant les résultats de l'opération lorsqu'elle est terminer, puis planifier une mise à jour de l'interface utilisateur sur le thread UI (FX Application). Tous les kits d'outils d'interface utilisateur à un seul thread ont un mécanisme pour le faire: dans JavaFX, vous pouvez le faire en appelant Platform.runLater(Runnable r) pour exécuter r.run() sur le FX Application Thread. (Dans Swing, vous pouvez appeler SwingUtilities.invokeLater(Runnable r) pour exécuter r.run() sur le thread de répartition des événements AWT.) JavaFX (voir plus loin dans cette réponse) fournit également une API de niveau supérieur pour gérer la communication retour au fil d'application FX.

Bonnes pratiques générales pour le multithreading

La meilleure pratique pour travailler avec plusieurs threads est de structurer le code qui doit être exécuté sur un thread "défini par l'utilisateur" en tant qu'objet qui est initialisé avec un état fixe, dispose d'une méthode pour effectuer l'opération et, à la fin, renvoie un objet représentant le résultat. L'utilisation d'objets immuables pour l'état initialisé et le résultat du calcul est hautement souhaitable. L'idée ici est d'éliminer la possibilité que tout état mutable soit visible à partir de plusieurs threads autant que possible. L'accès aux données d'une base de données correspond bien à cet idiome: vous pouvez initialiser votre objet "travailleur" avec les paramètres d'accès à la base de données (termes de recherche, etc.). Exécutez la requête de base de données et obtenez un jeu de résultats, utilisez le jeu de résultats pour remplir une collection d'objets de domaine et renvoyez la collection à la fin.

Dans certains cas, il sera nécessaire de partager l'état mutable entre plusieurs threads. Lorsque cela doit absolument être fait, vous devez synchroniser soigneusement l'accès à cet état pour éviter d'observer l'état dans un état incohérent (il y a d'autres problèmes plus subtils qui doivent être résolus, tels que la vivacité de l'état, etc.). La recommandation forte lorsque cela est nécessaire est d'utiliser une bibliothèque de haut niveau pour gérer ces complexités pour vous.

Utilisation de l'API javafx.concurrent

JavaFX fournit une API d'accès simultané conçue pour exécuter du code dans un thread d'arrière-plan, avec une API spécialement conçue pour mettre à jour l'interface utilisateur JavaFX à la fin (ou pendant) l'exécution de ce code. Cette API est conçue pour interagir avec l'API Java.util.concurrent , qui fournit des fonctionnalités générales pour écrire du code multithread (mais sans hook d'interface utilisateur). La classe clé dans javafx.concurrent Est Task , qui représente une unité de travail unique et unique destinée à être exécutée sur un thread d'arrière-plan. Cette classe définit une seule méthode abstraite, call(), qui ne prend aucun paramètre, renvoie un résultat et peut lever des exceptions vérifiées. Task implémente Runnable avec sa méthode run() en appelant simplement call(). Task possède également une collection de méthodes dont la mise à jour est garantie sur le thread d'application FX, telles que updateProgress(...) , updateMessage(...) , etc. Il définit certaines propriétés observables (par exemple state et value ): les écouteurs de ces propriétés seront informé des changements sur le thread d'application FX. Enfin, il existe quelques méthodes pratiques pour enregistrer les gestionnaires ( setOnSucceeded(...) , setOnFailed(...) , etc.); tous les gestionnaires enregistrés via ces méthodes seront également appelés sur le thread d'application FX.

Ainsi, la formule générale pour récupérer les données d'une base de données est la suivante:

  1. Créez un Task pour gérer l'appel à la base de données.
  2. Initialisez Task avec tout état nécessaire pour effectuer l'appel de base de données.
  3. Implémentez la méthode call() de la tâche pour effectuer l'appel de base de données, en renvoyant les résultats de l'appel.
  4. Enregistrez un gestionnaire avec la tâche d'envoyer les résultats à l'interface utilisateur lorsqu'elle est terminée.
  5. Appelez la tâche sur un thread d'arrière-plan.

Pour l'accès à la base de données, je recommande fortement d'encapsuler le code de base de données réel dans une classe distincte qui ne connaît rien de l'interface utilisateur ( modèle de conception d'objet d'accès aux données ). Demandez ensuite à la tâche d'appeler les méthodes sur l'objet d'accès aux données.

Vous pourriez donc avoir une classe DAO comme celle-ci (notez qu'il n'y a pas de code d'interface utilisateur ici):

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

La récupération d'un tas de widgets peut prendre beaucoup de temps, donc tout appel d'une classe d'interface utilisateur (par exemple une classe de contrôleur) doit planifier cela sur un thread d'arrière-plan. Une classe de contrôleur pourrait ressembler à ceci:

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

    // ...
}

Remarquez comment l'appel à la méthode DAO (potentiellement) longue durée est encapsulé dans un Task qui est exécuté sur un thread d'arrière-plan (via l'accesseur) pour éviter de bloquer l'interface utilisateur (règle 2 ci-dessus). La mise à jour de l'interface utilisateur (widgetTable.setItems(...)) est en fait exécutée sur le thread d'application FX, en utilisant la méthode de rappel de commodité de TasksetOnSucceeded(...) ( satisfaisant à la règle 1).

Dans votre cas, l'accès à la base de données que vous effectuez renvoie un seul résultat, vous pouvez donc avoir une méthode comme

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

    // ...
}

Et puis votre code de contrôleur ressemblerait

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

Les docs API pour Task ont beaucoup d'autres exemples, y compris la mise à jour de la propriété progress de la tâche (utile pour les barres de progression ..., etc.

48
James_D

Exception dans le thread "Thread A" Java.lang.IllegalStateException: Pas sur le thread d'application FX; currentThread = Thread A

L'exception tente de vous dire que vous essayez d'accéder au graphique de scène JavaFX en dehors du thread d'application JavaFX. Mais où ??

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

Si je ne peux pas faire cela, comment utiliser un fil d'arrière-plan?

Les différentes approches conduisent à des solutions similaires.

Enveloppez votre élément de graphique de scène avec Platform.runLater

La manière la plus simple et la plus simple consiste à envelopper la ligne ci-dessus dans Plaform.runLater, tel qu'il s'exécute sur le thread d'application JavaFX.

Platform.runLater(() -> courseCodeLbl.setText(rs.getString(1)));

Utiliser la tâche

La meilleure approche pour accompagner ces scénarios consiste à utiliser Tâche , qui dispose de méthodes spécialisées pour renvoyer les mises à jour. Dans l'exemple suivant, j'utilise updateMessage pour mettre à jour le message. Cette propriété est liée à 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();
    }
}
9
ItachiUchiha

Cela n'a rien à voir avec la base de données. JavaFx, comme à peu près toutes les bibliothèques GUI, nécessite que vous n'utilisiez que le thread d'interface utilisateur principal pour modifier l'interface graphique.

Vous devez transmettre les données de la base de données au thread d'interface utilisateur principal. Utilisez Platform.runLater () pour planifier l'exécution d'un Runnable dans le thread d'interface utilisateur principal.

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

Alternativement, vous pouvez tiliser la tâche .

4
Lie Ryan