Regras de encadeamento para JavaFX
Existem duas regras básicas para threads e JavaFX:
- 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. Algumas outras operações (por exemplo, criar um novo
Stage
s) também estão vinculados a esta regra. - Qualquer código que demore muito para ser executado deve ser executado em um encadeamento em segundo plano (ou seja, não no encadeamento do aplicativo FX).
A razão para a primeira regra é que, como a maioria dos kits de ferramentas de interface do usuário, a estrutura é escrita sem qualquer sincronização no estado dos elementos do gráfico de cena. Adicionar sincronização gera um custo de desempenho, e isso acaba sendo um custo proibitivo para kits de ferramentas de 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 FX Application Thread é 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 métodos sujeitos a essa regra executa verificações e lança exceções de tempo de execução se a regra for violada. (Isso contrasta com o Swing, onde você pode escrever código "ilegal" e ele pode parecer funcionar bem, mas na verdade é propenso a falhas aleatórias e imprevisíveis em momentos arbitrários.) Esta é a causa do a
IllegalStateException
você está vendo :você está chamando courseCodeLbl.setText(...)
de um thread que não seja o FX Application Thread. O motivo da segunda regra é que o FX Application Thread, além de ser responsável por processar os eventos do usuário, também é responsável pela renderização da cena. Portanto, se você executar uma operação de longa duração nesse encadeamento, a interface do usuário não será renderizada até que a operação seja concluída e não responderá aos eventos do usuário. Embora isso não gere exceções ou cause um estado de objeto corrompido (como a violação da regra 1 fará), isso (na melhor das hipóteses) cria uma experiência de usuário ruim.
Assim, se você tiver uma operação de longa duração (como acessar um banco de dados) que precise atualizar a interface do usuário na conclusão, o plano básico é executar a operação de longa duração em um thread em segundo plano, retornando os resultados da operação quando for concluir e, em seguida, agende uma atualização para a interface do usuário no thread da interface do usuário (aplicativo FX). Todos os kits de ferramentas de interface do usuário de thread único têm um mecanismo para fazer isso:no JavaFX, você pode fazer isso chamando
Platform.runLater(Runnable r)
para executar r.run()
no Tópico do Aplicativo FX. (No Swing, você pode chamar SwingUtilities.invokeLater(Runnable r)
para executar r.run()
no encadeamento de despacho de eventos AWT.) JavaFX (veja mais adiante nesta resposta) também fornece alguma API de nível superior para gerenciar a comunicação de volta ao encadeamento do aplicativo FX. Boas práticas gerais para multithreading
A melhor prática para trabalhar com vários threads é estruturar o código que deve ser executado em um thread "definido pelo usuário" como um objeto que é inicializado com algum estado fixo, tem um método para executar a operação e, ao concluir, retorna um objeto representando o resultado. O uso de objetos 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 threads o máximo possível. Acessar dados de um banco de dados se encaixa muito bem nesse idioma:você pode inicializar seu objeto "trabalhador" com os parâmetros para o acesso ao banco de dados (termos de pesquisa, etc). Execute a consulta de banco de dados e obtenha um conjunto de resultados, use o conjunto de resultados para preencher uma coleção de objetos de domínio e retorne a coleção no final.
Em alguns casos, será necessário compartilhar o estado mutável entre vários threads. Quando isso absolutamente precisa ser feito, você precisa sincronizar cuidadosamente o acesso a esse estado para evitar observar o estado em um estado inconsistente (há outras questões mais sutis que precisam ser abordadas, como a vitalidade do estado etc.). A forte recomendação quando isso é necessário é usar uma biblioteca de alto nível para gerenciar essas complexidades para você.
Usando a API javafx.concurrent
JavaFX fornece uma API de simultaneidade que é projetado para executar código em um thread em segundo plano, com API projetada especificamente para atualizar a interface do usuário do JavaFX na conclusão (ou durante) da execução desse código. Esta API foi projetada para interagir com o
java.util.concurrent
API
, que fornece recursos gerais para escrever código multithread (mas sem ganchos de interface do usuário). A classe de chave em javafx.concurrent
é Tarefa
, que representa uma unidade de trabalho única e única destinada a ser executada em um thread em segundo plano. Esta classe define um único método abstrato, call()
, que não recebe parâmetros, retorna um resultado e pode lançar exceções verificadas. Tarefa
implementa Executável
com seu run()
método simplesmente invocando call()
. Tarefa
também tem uma coleção de métodos que garantem a atualização do estado no FX Application Thread, como updateProgress(...)
, updateMessage(...)
, etc. Ele define algumas propriedades observáveis (por exemplo, estado
e valor
):os ouvintes dessas propriedades serão notificados sobre as alterações no FX Application Thread. Por fim, existem alguns métodos convenientes para registrar manipuladores (setOnSucceeded(...)
, setOnFailed(...)
, etc); quaisquer manipuladores registrados por meio desses métodos também serão invocados no FX Application Thread. Portanto, a fórmula geral para recuperar dados de um banco de dados é:
- Crie uma
Tarefa
para lidar com a chamada para o banco de dados. - Inicialize a
Tarefa
com qualquer estado necessário para realizar a chamada do banco de dados. - Implemente a
call()
da tarefa método para realizar a chamada do banco de dados, retornando os resultados da chamada. - Registre um gerenciador com a tarefa para enviar os resultados para a interface do usuário quando ela for concluída.
- Chame a tarefa em um thread em segundo plano.
Para acesso ao banco de dados, recomendo enfaticamente encapsular o código real do banco de dados em uma classe separada que não sabe nada sobre a interface do usuário ( Padrão de design do objeto de acesso a dados ). Em seguida, basta que a tarefa invoque os métodos no objeto de acesso a dados.
Portanto, você pode ter uma classe DAO como esta (observe que não há código de interface do usuário 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<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();
}
}
Recuperar vários widgets pode levar muito tempo, portanto, qualquer chamada de uma classe de interface do usuário (por exemplo, uma classe de controlador) deve agendar isso em um thread em segundo plano. Uma classe de controlador 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<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);
}
// ...
}
Observe como a chamada para o método DAO (potencialmente) de longa duração é encapsulada em uma
Task
que é executado em um thread em segundo plano (através 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(...)
) é realmente executado de volta no FX Application Thread, usando o Task
método de retorno de chamada de conveniência setOnSucceeded(...)
(cumprindo a regra 1). No seu caso, o acesso ao banco de dados que você está realizando 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<Course>...
return null ;
}
}
}
// ...
}
E então o código do seu controlador ficaria assim
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);
Os documentos de API para
Tarefa
tem muitos outros exemplos, incluindo a atualização do progress
propriedade da tarefa (útil para barras de progresso..., etc.