Threading-regels voor JavaFX
Er zijn twee basisregels voor threads en JavaFX:
- Elke code die de status van een knooppunt dat deel uitmaakt van een scènegrafiek wijzigt of toegang verschaft, moet worden uitgevoerd op de JavaFX-toepassingsthread. Bepaalde andere bewerkingen (bijv. het maken van een nieuwe
Stage
s) zijn ook gebonden aan deze regel. - Elke code die lang kan duren om uit te voeren moet worden uitgevoerd op een achtergrondthread (d.w.z. niet op de FX Application Thread).
De reden voor de eerste regel is dat, zoals de meeste UI-toolkits, het raamwerk is geschreven zonder enige synchronisatie op de status van elementen van de scènegrafiek. Het toevoegen van synchronisatie brengt prestatiekosten met zich mee, en dit blijkt een onbetaalbare prijs te zijn voor UI-toolkits. Er kan dus maar één thread veilig toegang krijgen tot deze status. Aangezien de UI-thread (FX Application Thread voor JavaFX) toegang moet hebben tot deze status om de scène weer te geven, is de FX Application Thread de enige thread waarop u toegang hebt tot de "live"-scènegrafiekstatus. In JavaFX 8 en later voeren de meeste methoden die onder deze regel vallen, controles uit en genereren runtime-uitzonderingen als de regel wordt geschonden. (Dit in tegenstelling tot Swing, waar je "illegale" code kunt schrijven en het lijkt misschien goed te werken, maar is in feite vatbaar voor willekeurige en onvoorspelbare fouten op een willekeurig moment.) Dit is de oorzaak van de IllegalStateException
je ziet :je belt courseCodeLbl.setText(...)
van een andere thread dan de FX Application Thread.
De reden voor de tweede regel is dat de FX-toepassingsthread niet alleen verantwoordelijk is voor het verwerken van gebruikersgebeurtenissen, maar ook voor het weergeven van de scène. Dus als u een langlopende bewerking op die thread uitvoert, wordt de gebruikersinterface niet weergegeven totdat die bewerking is voltooid en reageert deze niet meer op gebruikersgebeurtenissen. Hoewel dit geen uitzonderingen genereert of een corrupte objectstatus veroorzaakt (zoals het overtreden van regel 1 zal doen), creëert het (in het beste geval) een slechte gebruikerservaring.
Dus als u een langlopende bewerking hebt (zoals toegang tot een database) die de gebruikersinterface moet bijwerken na voltooiing, is het basisplan om de langlopende bewerking uit te voeren in een achtergrondthread, waarbij de resultaten van de bewerking worden geretourneerd wanneer deze is voltooid. voltooien en plan vervolgens een update voor de gebruikersinterface op de UI (FX Application)-thread. Alle single-threaded UI-toolkits hebben een mechanisme om dit te doen:in JavaFX kunt u dit doen door Platform.runLater(Runnable r)
aan te roepen om r.run()
uit te voeren op de FX-toepassingsthread. (In Swing kun je SwingUtilities.invokeLater(Runnable r)
aanroepen om r.run()
uit te voeren op de AWT-gebeurtenisverzendingsthread.) JavaFX (zie verderop in dit antwoord) biedt ook een API van een hoger niveau voor het beheren van de communicatie terug naar de FX Application Thread.
Algemene goede praktijken voor multithreading
De beste werkwijze voor het werken met meerdere threads is om code die moet worden uitgevoerd op een "door de gebruiker gedefinieerde" thread te structureren als een object dat is geïnitialiseerd met een vaste status, een methode heeft om de bewerking uit te voeren en na voltooiing een object retourneert het resultaat vertegenwoordigen. Het gebruik van onveranderlijke objecten voor de geïnitialiseerde toestand en het berekeningsresultaat is zeer wenselijk. Het idee hier is om de mogelijkheid te elimineren dat een veranderlijke toestand zichtbaar is vanuit meerdere threads, voor zover mogelijk. Toegang tot gegevens uit een database past goed in dit idioom:u kunt uw "worker"-object initialiseren met de parameters voor de databasetoegang (zoektermen, enz.). Voer de databasequery uit en verkrijg een resultatenset, gebruik de resultatenset om een verzameling domeinobjecten te vullen en retourneer de verzameling aan het einde.
In sommige gevallen is het nodig om de veranderlijke status tussen meerdere threads te delen. Wanneer dit absoluut moet worden gedaan, moet u de toegang tot die staat zorgvuldig synchroniseren om te voorkomen dat de staat in een inconsistente staat wordt waargenomen (er zijn andere, meer subtiele problemen die moeten worden aangepakt, zoals levendigheid van de staat, enz.). De sterke aanbeveling wanneer dit nodig is, is om een bibliotheek op hoog niveau te gebruiken om deze complexiteiten voor u te beheren.
De javafx.concurrent API gebruiken
JavaFX biedt een gelijktijdigheids-API
dat is ontworpen voor het uitvoeren van code in een achtergrondthread, met een API die specifiek is ontworpen voor het bijwerken van de JavaFX-gebruikersinterface na voltooiing van (of tijdens) de uitvoering van die code. Deze API is ontworpen om te communiceren met de java.util.concurrent
API
, die algemene faciliteiten biedt voor het schrijven van multithreaded-code (maar zonder UI-hooks). De sleutelklasse in javafx.concurrent
is Task
, die een enkele, eenmalige werkeenheid vertegenwoordigt die bedoeld is om op een achtergronddraad te worden uitgevoerd. Deze klasse definieert een enkele abstracte methode, call()
, die geen parameters nodig heeft, een resultaat retourneert en gecontroleerde uitzonderingen kan genereren. Task
implementeert Runnable
met zijn run()
methode die eenvoudig call()
. aanroept . Task
heeft ook een verzameling methoden die gegarandeerd de status van de FX Application Thread bijwerken, zoals updateProgress(...)
, updateMessage(...)
, etc. Het definieert enkele waarneembare eigenschappen (bijv. state
en value
):luisteraars van deze eigenschappen worden op de hoogte gebracht van wijzigingen in de FX Application Thread. Ten slotte zijn er enkele handige methoden om handlers te registreren (setOnSucceeded(...)
, setOnFailed(...)
, enzovoort); alle handlers die via deze methoden zijn geregistreerd, worden ook aangeroepen in de FX Application Thread.
Dus de algemene formule voor het ophalen van gegevens uit een database is:
- Maak een
Task
om de oproep naar de database af te handelen. - Initialiseer de
Task
met elke status die nodig is om de database-aanroep uit te voeren. - Implementeer de
call()
. van de taak methode om de database-aanroep uit te voeren, waarbij de resultaten van de aanroep worden geretourneerd. - Registreer een handler met de taak om de resultaten naar de gebruikersinterface te sturen wanneer deze is voltooid.
- Roep de taak op in een achtergrondthread.
Voor databasetoegang raad ik ten zeerste aan om de daadwerkelijke databasecode in te kapselen in een aparte klasse die niets weet over de gebruikersinterface ( Ontwerppatroon voor gegevenstoegangsobject ). Laat de taak vervolgens de methoden op het gegevenstoegangsobject aanroepen.
Dus misschien heb je een DAO-klasse zoals deze (let op:er is hier geen UI-code):
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();
}
}
Het ophalen van een aantal widgets kan lang duren, dus alle oproepen van een UI-klasse (bijvoorbeeld een controller-klasse) zouden dit op een achtergrondthread moeten plannen. Een controllerklasse kan er als volgt uitzien:
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);
}
// ...
}
Merk op hoe de aanroep van de (potentieel) langlopende DAO-methode is verpakt in een Task
die wordt uitgevoerd op een achtergrondthread (via de accessor) om te voorkomen dat de gebruikersinterface wordt geblokkeerd (regel 2 hierboven). De update van de gebruikersinterface (widgetTable.setItems(...)
) wordt daadwerkelijk terug uitgevoerd op de FX Application Thread, met behulp van de Task
's gemak callback-methode setOnSucceeded(...)
(voldoet aan regel 1).
In jouw geval levert de databasetoegang die je uitvoert een enkel resultaat op, dus je hebt misschien een methode als
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 ;
}
}
}
// ...
}
En dan zou je controllercode er als volgt uitzien:
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);
De API-documenten voor Task
heb nog veel meer voorbeelden, waaronder het bijwerken van de progress
eigenschap van de taak (handig voor voortgangsbalken..., enz.