Usando encadeamentos para fazer solicitações de database

Eu estou tentando entender como threads funciona em java. Esta é uma solicitação de database simples que retorna um ResultSet. Estou usando o 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(); } } } 

Isso retorna uma exceção:

Exceção no thread “Thread A” java.lang.IllegalStateException: Não no thread do aplicativo FX; currentThread = Thread A

Como implementar corretamente o encadeamento para que cada solicitação de database seja executada em um segundo encadeamento em vez do encadeamento principal?

Eu já ouvi falar da implementação do Runnable, mas como eu invoco diferentes methods no método run?

Nunca trabalhei com segmentação antes, mas achei que é hora de isso.

Regras de Encadeamento para o JavaFX

Existem duas regras básicas para encadeamentos e JavaFX:

  1. Qualquer código que modifique ou acesse o estado de um nó que faz parte de um gráfico de cena deve ser executado no encadeamento do aplicativo JavaFX. Certas outras operações (por exemplo, criando novos Stage ) também estão vinculadas a esta regra.
  2. Qualquer código que pode levar muito tempo para ser executado deve ser executado em um encadeamento de segundo plano (ou seja, não no encadeamento do aplicativo FX).

O motivo da primeira regra é que, como a maioria dos kits de ferramentas da interface do usuário, a estrutura é gravada sem nenhuma synchronization no estado dos elementos do gráfico de cena. Adicionar synchronization gera um custo de desempenho, e isso acaba sendo um custo proibitivo para os kits de ferramentas da interface do usuário. Assim, apenas um thread pode acessar com segurança esse estado. Como o thread de interface do usuário (FX Application Thread para JavaFX) precisa acessar esse estado para renderizar a cena, o Thread de aplicativo FX é o único thread no qual você pode acessar o estado do gráfico de cena “ao vivo”. No JavaFX 8 e posterior, a maioria dos methods sujeitos a essa regra executa verificações e lança exceções de tempo de execução se a regra for violada. (Isso está em contraste com o Swing, onde você pode escrever código “ilegal” e pode parecer rodar bem, mas está propenso a falhas aleatórias e imprevisíveis no momento arbitrário.) Esta é a causa da IllegalStateException você está vendo : você está chamando courseCodeLbl.setText(...) de um thread diferente do Thread de aplicativo FX.

O motivo da segunda regra é que o Thread de Aplicação FX, além de ser responsável pelo processamento de events do usuário, também é responsável pela renderização da cena. Portanto, se você executar uma operação demorada nesse encadeamento, a IU não será renderizada até que a operação seja concluída e não responda aos events do usuário. Embora isso não gere exceções ou cause estado de object corrompido (como violará a regra 1), ele (na melhor das hipóteses) cria uma experiência de usuário ruim.

Portanto, se você tiver uma operação de execução demorada (como acessar um database) que precisa atualizar a UI ao concluir, o plano básico é executar a operação de longa duração em um encadeamento em segundo plano, retornando os resultados da operação quando ela for executada. completo e, em seguida, agendar uma atualização para a interface do usuário no thread da interface do usuário (aplicativo de FX). Todos os toolkits de interface única do usuário têm um mecanismo para fazer isso: no JavaFX, você pode fazer isso chamando Platform.runLater(Runnable r) para executar o r.run() no Thread do aplicativo FX. (No Swing, é possível chamar SwingUtilities.invokeLater(Runnable r) para executar o r.run() no encadeamento de expedição de events do AWT.) O JavaFX (veja mais adiante nesta resposta) também fornece uma API de alto nível para gerenciar a comunicação. o thread do aplicativo FX.

Boas Práticas Gerais para Multithreading

A melhor prática para trabalhar com vários encadeamentos é estruturar o código a ser executado em um encadeamento “definido pelo usuário” como um object que é inicializado com algum estado fixo, possui um método para executar a operação e, na conclusão, retorna um object representando o resultado. Usar objects imutáveis ​​para o estado inicializado e o resultado da computação é altamente desejável. A ideia aqui é eliminar a possibilidade de qualquer estado mutável ser visível de vários encadeamentos, tanto quanto possível. Acessar dados de um database se encheckbox perfeitamente nesse idioma: você pode inicializar seu object “trabalhador” com os parâmetros para o access ao database (termos de pesquisa, etc). Execute a consulta do database e obtenha um conjunto de resultados, use o conjunto de resultados para preencher uma coleção de objects de domínio e retorne a coleção no final.

Em alguns casos, será necessário compartilhar o estado mutável entre vários encadeamentos. Quando isso absolutamente precisa ser feito, é necessário sincronizar cuidadosamente o access a esse estado para evitar a observação do estado em um estado inconsistente (há outros problemas mais sutis que precisam ser resolvidos, como a vivacidade do estado etc.). A recomendação forte quando isso é necessário é usar uma biblioteca de alto nível para gerenciar essas complexidades para você.

Usando a API javafx.concurrent

O JavaFX fornece uma API de simultaneidade projetada para executar código em um thread de segundo plano, com API especificamente projetada para atualizar a UI do JavaFX na conclusão (ou durante) a execução desse código. Essa API foi projetada para interagir com a API java.util.concurrent , que fornece resources gerais para escrever código multithread (mas sem ganchos da interface do usuário). A class principal em javafx.concurrent é Task , que representa uma única unidade de trabalho destinada a ser executada em um thread de segundo plano. Essa class define um único método abstrato, call() , que não recebe parâmetros, retorna um resultado e pode lançar exceções verificadas. Task implementa Runnable com seu método run() simplesmente invocando call() . Task também possui uma coleção de methods que têm a garantia de atualizar o estado no Thread do aplicativo FX, como updateProgress(...) , updateMessage(...) , etc. Ele define algumas propriedades observáveis ​​(por exemplo, state e value ): listeners para essas propriedades será notificado de alterações no Thread de aplicativo FX. Finalmente, existem alguns methods convenientes para registrar manipuladores ( setOnSucceeded(...) , setOnFailed(...) , etc); qualquer manipulador registrado por esses methods também será invocado no Thread de Aplicação FX.

Portanto, a fórmula geral para recuperar dados de um database é:

  1. Crie uma Task para lidar com a chamada para o database.
  2. Inicialize a Task com qualquer estado necessário para executar a chamada do database.
  3. Implemente o método call() da tarefa para executar a chamada do database, retornando os resultados da chamada.
  4. Registre um manipulador com a tarefa para enviar os resultados para a interface do usuário quando ela estiver concluída.
  5. Invoque a tarefa em um thread de segundo plano.

Para access ao database, recomendo enfaticamente encapsular o código real do database em uma class separada que não saiba nada sobre a IU ( padrão de design do Data Access Object ). Em seguida, basta que a tarefa invoque os methods no object de access a dados.

Então você pode ter uma class DAO como essa (note que não há código UI aqui):

 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 um monte de widgets pode levar um longo tempo, portanto, qualquer chamada de uma class de interface do usuário (por exemplo, uma class de controlador) deve agendar isso em um thread de segundo plano. Uma class controladora pode ser assim:

 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 como a chamada para o método DAO (potencialmente) de execução longa é agrupada em uma Task que é executada em um thread de segundo plano (por meio do acessador) para evitar o bloqueio da interface do usuário (regra 2 acima). A atualização para a interface do usuário ( widgetTable.setItems(...) ) é executada de fato no widgetTable.setItems(...) do aplicativo FX, usando o método de retorno de chamada conveniente da Task setOnSucceeded(...) (satisfazendo a regra 1).

No seu caso, o access ao database que você está executando retorna um único resultado, então você pode ter um 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 ; } } } // ... } 

E então o código do seu controlador seria semelhante

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

Os documentos da API para Task têm muito mais exemplos, incluindo a atualização da propriedade de progress da tarefa (útil para barras de progresso …, etc.

Exceção no thread “Thread A” java.lang.IllegalStateException: Não no thread do aplicativo FX; currentThread = Thread A

A exceção é tentar dizer que você está tentando acessar o gráfico de cena do JavaFX fora do encadeamento do aplicativo JavaFX. Mas onde ??

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

Se eu não posso fazer isso, como eu uso um thread de fundo?

Existem diferentes abordagens que levam a soluções semelhantes.

Embrulhe-o Elemento do gráfico de cena com Platform.runLater

A maneira mais fácil e mais simples é envolver a linha acima no Plaform.runLater , de tal forma que ela seja executada no encadeamento do Aplicativo JavaFX.

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

Use a tarefa

A melhor abordagem para acompanhar esses cenários é usar o Task , que possui methods especializados para enviar atualizações. No exemplo a seguir, estou usando updateMessage para atualizar a mensagem. Esta propriedade é courseCodeLbl a courseCodeLbl courseCodeLbl.

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

Isso não tem nada a ver com database. O JavaFx, como praticamente todas as bibliotecas GUI, requer que você use apenas o thread principal da interface do usuário para modificar a GUI.

Você precisa passar os dados do database de volta ao thread principal da interface do usuário. Use Platform.runLater () para agendar um Runnable para ser executado no thread principal da interface do usuário.

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

Como alternativa, você pode usar a tarefa .