Usar hilos para hacer solicitudes de bases de datos

Estoy tratando de entender cómo funcionan los hilos en Java. Esta es una solicitud de base de datos simple que devuelve un ResultSet. Estoy usando 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.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(); } } } 

Esto devuelve una excepción:

Excepción en el hilo “Subproceso A” java.lang.IllegalStateException: No en el hilo de la aplicación FX; currentThread = Hilo A

¿Cómo implemento correctamente el subprocesamiento para que cada solicitud de base de datos se ejecute en un segundo subproceso en lugar del subproceso principal?

He oído hablar de la implementación de Runnable, pero ¿cómo invoco diferentes métodos en el método de ejecución?

Nunca he trabajado con Threading antes, pero pensé que era hora de hacerlo.

Threading Rules para JavaFX

Hay dos reglas básicas para hilos y JavaFX:

  1. Cualquier código que modifique o acceda al estado de un nodo que sea parte de un gráfico de escena se debe ejecutar en el hilo de la aplicación JavaFX. Algunas otras operaciones (por ejemplo, la creación de nuevas Stage ) también están sujetas a esta regla.
  2. Cualquier código que tarde mucho tiempo en ejecutarse se debe ejecutar en un hilo de fondo (es decir, no en el hilo de la aplicación FX).

El motivo de la primera regla es que, como la mayoría de los juegos de herramientas de interfaz de usuario, el marco se escribe sin ninguna sincronización en el estado de los elementos del gráfico de escena. Agregar sincronización implica un costo de rendimiento, y esto resulta ser un costo prohibitivo para los kits de herramientas de interfaz de usuario. Por lo tanto, solo un hilo puede acceder de forma segura a este estado. Como el subproceso UI (FX Application Thread for JavaFX) necesita acceder a este estado para renderizar la escena, el subproceso FX Application es el único subproceso en el que puede acceder al estado del gráfico de escena “en vivo”. En JavaFX 8 y posterior, la mayoría de los métodos sujetos a esta regla realizan comprobaciones y lanzan excepciones de tiempo de ejecución si se infringe la regla. (Esto está en contraste con Swing, donde puede escribir código “ilegal” y parece funcionar bien, pero de hecho es propenso a fallas aleatorias e impredecibles en un tiempo arbitrario). Esta es la causa de la IllegalStateException que está viendo : está llamando a courseCodeLbl.setText(...) desde un hilo que no sea el hilo de la aplicación FX.

El motivo de la segunda regla es que el subproceso de la aplicación FX, además de ser responsable del procesamiento de los eventos del usuario, también es responsable de representar la escena. Por lo tanto, si realiza una operación de larga ejecución en ese subproceso, la interfaz de usuario no se representará hasta que se complete esa operación y dejará de responder a los eventos del usuario. Si bien esto no generará excepciones ni provocará un estado de objeto corrupto (ya que violará la regla 1), (en el mejor de los casos) crea una experiencia de usuario deficiente.

Por lo tanto, si tiene una operación de larga ejecución (como acceder a una base de datos) que necesita actualizar la UI al finalizar, el plan básico es realizar la operación de larga ejecución en una cadena de fondo, devolviendo los resultados de la operación cuando es complete y luego programe una actualización de la interfaz de usuario en el subproceso UI (aplicación FX). Todos los toolkits UI de subproceso único tienen un mecanismo para hacer esto: en JavaFX puede hacerlo llamando a Platform.runLater(Runnable r) para ejecutar r.run() en el subproceso de la aplicación FX. (En Swing, puede llamar a SwingUtilities.invokeLater(Runnable r) para ejecutar r.run() en el subproceso AWT de distribución de eventos.) JavaFX (consulte más adelante en esta respuesta) también proporciona API de mayor nivel para administrar la comunicación de nuevo a el hilo de la aplicación FX.

Buenas Prácticas Generales para Multithreading

La mejor práctica para trabajar con múltiples subprocesos es estructurar el código que se debe ejecutar en un subproceso “definido por el usuario” como un objeto que se inicializa con un estado fijo, tiene un método para realizar la operación y, al finalizar, devuelve un objeto representando el resultado El uso de objetos inmutables para el estado inicializado y el resultado de cálculo es altamente deseable. La idea aquí es eliminar la posibilidad de que cualquier estado mutable sea visible desde múltiples subprocesos en la medida de lo posible. El acceso a los datos de una base de datos se adapta muy bien a esta expresión idiomática: puede inicializar su objeto “trabajador” con los parámetros para el acceso a la base de datos (términos de búsqueda, etc.). Realice la consulta de la base de datos y obtenga un conjunto de resultados, utilice el conjunto de resultados para poblar una colección de objetos de dominio y devuelva la colección al final.

En algunos casos, será necesario compartir el estado mutable entre múltiples hilos. Cuando esto sea absolutamente necesario, debe sincronizar cuidadosamente el acceso a ese estado para evitar observar el estado en un estado incoherente (hay otros problemas más sutiles que deben abordarse, como la vida del estado, etc.). La recomendación fuerte cuando esto es necesario es utilizar una biblioteca de alto nivel para administrar estas complejidades para usted.

Usando la API javafx.concurrent

JavaFX proporciona una API de concurrencia diseñada para ejecutar código en una cadena de fondo, con API diseñada específicamente para actualizar la interfaz de usuario de JavaFX al completar (o durante) la ejecución de ese código. Esta API está diseñada para interactuar con la API java.util.concurrent , que proporciona java.util.concurrent generales para escribir código multiproceso (pero sin ganchos de UI). La clase de clave en javafx.concurrent es Task , que representa una unidad de trabajo única, única, destinada a realizarse en una cadena de fondo. Esta clase define un único método abstracto, call() , que no toma parámetros, devuelve un resultado y puede arrojar excepciones marcadas. Task implementa Runnable con su método run() simplemente invocando a call() . Task también tiene una colección de métodos que tienen la garantía de actualizar el estado en el subproceso de la aplicación FX, como updateProgress(...) , updateMessage(...) , etc. Define algunas propiedades observables (por ejemplo, state y value ): oyentes a estas propiedades se les notificarán los cambios en el subproceso de la aplicación FX. Finalmente, hay algunos métodos de conveniencia para registrar manejadores ( setOnSucceeded(...) , setOnFailed(...) , etc.); cualquier controlador registrado a través de estos métodos también se invocará en el subproceso de la aplicación FX.

Entonces, la fórmula general para recuperar datos de una base de datos es:

  1. Cree una Task para manejar la llamada a la base de datos.
  2. Inicialice la Task con cualquier estado que sea necesario para realizar la llamada a la base de datos.
  3. Implemente el método call() la tarea para realizar la llamada a la base de datos y devolver los resultados de la llamada.
  4. Registre un controlador con la tarea para enviar los resultados a la UI cuando se complete.
  5. Invoque la tarea en un hilo de fondo.

Para el acceso a la base de datos, recomiendo encapsular el código de la base de datos real en una clase separada que no sabe nada acerca de la UI ( patrón de diseño del objeto de acceso a datos ). Luego simplemente haga que la tarea invoque los métodos en el objeto de acceso a datos.

Por lo tanto, es posible que tenga una clase DAO como esta (tenga en cuenta que aquí no hay código de 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 getWidgetsByType(String type) throws SQLException { try (PreparedStatement pstmt = conn.prepareStatement("select * from widget where type = ?")) { pstmt.setString(1, type); ResultSet rs = pstmt.executeQuery(); List 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(); } } 

Recuperar un montón de widgets puede llevar mucho tiempo, por lo que cualquier llamada de una clase UI (por ejemplo, una clase de controlador) debe progtwigr esto en una cadena de fondo. Una clase de controlador puede verse así:

 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 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> widgetSearchTask = new Task>() { @Override public List 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); } // ... } 

Observe cómo la llamada al método DAO (potencialmente) de larga ejecución se ajusta a una Task que se ejecuta en un hilo de fondo (a través del descriptor de acceso) para evitar el locking de la IU (regla 2 anterior). La actualización de la UI ( widgetTable.setItems(...) ) se ejecuta en realidad en el subproceso de la aplicación FX, utilizando el método de callback de la Task setOnSucceeded(...) (regla de satisfacción 1).

En su caso, el acceso a la base de datos que está realizando arroja un único resultado, por lo que puede tener un método como

 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... return null ; } } } // ... } 

Y luego su código de controlador se vería como

 final int courseCode = Integer.valueOf(courseId.getText()); Task courseTask = new Task() { @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); 

Los documentos API para Task tienen muchos más ejemplos, incluida la actualización de la propiedad de progress de la tarea (útil para barras de progreso …, etc.

Excepción en el hilo “Subproceso A” java.lang.IllegalStateException: No en el hilo de la aplicación FX; currentThread = Hilo A

La excepción es intentar decirle que está intentando acceder al gráfico de escena de JavaFX fuera de la secuencia de la aplicación JavaFX. Pero donde ??

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

Si no puedo hacer esto, ¿cómo uso un hilo de fondo?

Los diferentes enfoques conducen a soluciones similares.

Envuelve tu elemento de gráfico de escena con Platform.runLater

La forma más simple y sencilla es ajustar la línea anterior en Plaform.runLater , de modo que se ejecute en el hilo de la aplicación JavaFX.

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

Usar tarea

El mejor enfoque para ir con estos escenarios es usar Tarea , que tiene métodos especializados para enviar actualizaciones. En el siguiente ejemplo, estoy usando updateMessage para actualizar el mensaje. Esta propiedad está courseCodeLbl a courseCodeLbl textProperty.

 Task task = new Task() { @Override public Void call() { String courseName = ""; Course c = new Course(); c.setCCode(Integer.valueOf(courseId.getText())); mController = new ModelController(c); try { ResultSet rs = mController.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(); } } 

Esto no tiene nada que ver con la base de datos. JavaFx, como casi todas las bibliotecas de GUI, requiere que solo use el hilo de la interfaz de usuario principal para modificar la GUI.

Debe volver a pasar los datos de la base de datos al hilo principal de la IU. Utilice Platform.runLater () para progtwigr una Runnable que se ejecutará en el hilo principal de UI.

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

Alternativamente, puede usar Tarea .