Pravidla pro vytváření vláken pro JavaFX
Pro vlákna a JavaFX platí dvě základní pravidla:
- Jakýkoli kód, který upravuje nebo přistupuje ke stavu uzlu, který je součástí grafu scény, musí být spuštěn na aplikačním vláknu JavaFX. Některé další operace (např. vytvoření nové
Stage
s) jsou tímto pravidlem rovněž vázáni. - Jakýkoli kód, jehož spuštění může trvat dlouho, měl by být spuštěn ve vláknu na pozadí (tj. ne ve vláknu aplikace FX).
Důvodem prvního pravidla je, že stejně jako většina sad nástrojů uživatelského rozhraní je framework napsán bez jakékoli synchronizace stavu prvků grafu scény. Přidání synchronizace s sebou nese náklady na výkon, což se pro sady nástrojů uživatelského rozhraní ukazuje jako příliš vysoké náklady. K tomuto stavu tedy může bezpečně přistupovat pouze jedno vlákno. Vzhledem k tomu, že vlákno UI (FX Application Thread for JavaFX) potřebuje k vykreslení scény přístup k tomuto stavu, je vlákno FX Application Thread jediným vláknem, na kterém můžete přistupovat ke stavu grafu „živé“ scény. V JavaFX 8 a novějších většina metod podléhajících tomuto pravidlu provádí kontroly a vyvolává výjimky za běhu, pokud je pravidlo porušeno. (To je na rozdíl od Swing, kde můžete napsat "nelegální" kód a může se zdát, že běží dobře, ale ve skutečnosti je náchylný k náhodnému a nepředvídatelnému selhání v libovolném čase.) Toto je příčina IllegalStateException
vidíte :voláte courseCodeLbl.setText(...)
z jiného vlákna, než je vlákno aplikace FX.
Důvodem pro druhé pravidlo je, že vlákno aplikace FX, kromě toho, že je zodpovědné za zpracování uživatelských událostí, je také zodpovědné za vykreslení scény. Pokud tedy na tomto vláknu provedete dlouhotrvající operaci, uživatelské rozhraní se nevykreslí, dokud nebude operace dokončena, a přestane reagovat na uživatelské události. I když to negeneruje výjimky ani nezpůsobí poškozený stav objektu (jako porušení pravidla 1), způsobí to (v nejlepším případě) špatnou uživatelskou zkušenost.
Pokud tedy máte dlouhotrvající operaci (například přístup k databázi), která potřebuje po dokončení aktualizovat uživatelské rozhraní, základním plánem je provést dlouhotrvající operaci ve vláknu na pozadí a vrátit výsledky operace, když je dokončete a poté naplánujte aktualizaci uživatelského rozhraní ve vláknu UI (FX Application). Všechny sady nástrojů s jedním vláknem uživatelského rozhraní mají mechanismus, jak to udělat:v JavaFX to můžete provést voláním Platform.runLater(Runnable r)
pro provedení r.run()
na vláknu aplikace FX. (Ve Swing můžete zavolat SwingUtilities.invokeLater(Runnable r)
pro provedení r.run()
na vláknu odeslání události AWT.) JavaFX (viz dále v této odpovědi) také poskytuje některé API vyšší úrovně pro správu komunikace zpět do vlákna aplikace FX.
Obecné osvědčené postupy pro vícevláknové zpracování
Nejlepším postupem pro práci s více vlákny je strukturovat kód, který má být spuštěn na „uživatelsky definovaném“ vláknu jako objekt, který je inicializován s určitým pevným stavem, má metodu k provedení operace a po dokončení vrátí objekt. představující výsledek. Použití neměnných objektů pro inicializovaný stav a výsledek výpočtu je vysoce žádoucí. Cílem je co nejvíce eliminovat možnost, že jakýkoli proměnlivý stav bude viditelný z více vláken. Přístup k datům z databáze tomuto idiomu pěkně odpovídá:můžete inicializovat svůj "pracovní" objekt s parametry pro přístup k databázi (hledané výrazy atd.). Proveďte databázový dotaz a získejte sadu výsledků, použijte sadu výsledků k naplnění kolekce doménových objektů a na konci kolekci vraťte.
V některých případech bude nutné sdílet proměnlivý stav mezi více vlákny. Když je to absolutně nutné, musíte pečlivě synchronizovat přístup k tomuto stavu, abyste se vyhnuli pozorování stavu v nekonzistentním stavu (existují další jemnější problémy, které je třeba řešit, jako je živost stavu atd.). Důrazné doporučení, když je to potřeba, je použít knihovnu na vysoké úrovni, která tyto složitosti zvládne za vás.
Použití rozhraní API javafx.concurrent
JavaFX poskytuje concurrency API
který je navržen pro spouštění kódu ve vláknu na pozadí, s API speciálně navrženým pro aktualizaci uživatelského rozhraní JavaFX po dokončení (nebo během) provádění tohoto kódu. Toto rozhraní API je navrženo pro interakci s java.util.concurrent
API
, který poskytuje obecné možnosti pro psaní vícevláknového kódu (ale bez háčků uživatelského rozhraní). Třída klíče v javafx.concurrent
je Úkol
, který představuje jedinou jednorázovou jednotku práce, která má být provedena na vláknu na pozadí. Tato třída definuje jedinou abstraktní metodu, call()
, který nebere žádné parametry, vrací výsledek a může vyvolat zaškrtnuté výjimky. Úkol
implementuje Runnable
s jeho run()
metoda jednoduše vyvolá call()
. Úkol
má také sbírku metod, u kterých je zaručeno, že aktualizují stav ve vláknu aplikace FX, jako je updateProgress(...)
, updateMessage(...)
, atd. Definuje některé pozorovatelné vlastnosti (např. stav
a hodnota
):posluchači těchto vlastností budou upozorněni na změny ve vláknu aplikace FX. Nakonec existuje několik pohodlných metod pro registraci obslužných programů (setOnSucceeded(...)
, setOnFailed(...)
, atd); všechny obslužné rutiny registrované prostřednictvím těchto metod budou také vyvolány ve vláknu aplikace FX.
Obecný vzorec pro získávání dat z databáze je tedy:
- Vytvořte
Úkol
pro zpracování volání do databáze. - Inicializujte
Úlohu
s jakýmkoli stavem, který je potřebný k provedení volání databáze. - Implementujte
call()
úkolu metoda k provedení volání databáze, která vrátí výsledky volání. - Zaregistrujte obslužný program s úkolem odeslat výsledky do uživatelského rozhraní, až bude dokončen.
- Vyvolejte úlohu ve vláknu na pozadí.
Pro přístup k databázi důrazně doporučuji zapouzdřit skutečný kód databáze do samostatné třídy, která neví nic o uživatelském rozhraní ( Vzor návrhu objektu pro přístup k datům ). Pak stačí, aby úloha vyvolala metody na objektu přístupu k datům.
Takže můžete mít třídu DAO, jako je tato (všimněte si, že zde není žádný kód uživatelského rozhraní):
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();
}
}
Načítání hromady widgetů může trvat dlouho, takže všechna volání z třídy uživatelského rozhraní (např. třídy řadiče) by to měla naplánovat na vlákno na pozadí. Třída kontroleru může vypadat takto:
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);
}
// ...
}
Všimněte si, jak je volání (potenciálně) dlouhotrvající metody DAO zabaleno do Task
který je spuštěn na vláknu na pozadí (prostřednictvím přístupového objektu), aby se zabránilo blokování uživatelského rozhraní (pravidlo 2 výše). Aktualizace uživatelského rozhraní (widgetTable.setItems(...)
) se ve skutečnosti spustí zpět na vláknu aplikace FX pomocí Task
výhodná metoda zpětného volání setOnSucceeded(...)
(splňující pravidlo 1).
Ve vašem případě přístup k databázi, který provádíte, vrátí jediný výsledek, takže můžete mít metodu jako
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 ;
}
}
}
// ...
}
A pak by váš kód ovladače vypadal takto
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);
Dokumenty API pro Task
mají mnoho dalších příkladů, včetně aktualizace pokroku
vlastnost úkolu (užitečné pro ukazatele průběhu... atd.