Trådningsregler för JavaFX
Det finns två grundläggande regler för trådar och JavaFX:
- All kod som modifierar eller kommer åt tillståndet för en nod som är en del av en scengraf måste exekveras på JavaFX-applikationstråden. Vissa andra operationer (t.ex. skapa ny
Stage
s) är också bundna av denna regel. - All kod som kan ta lång tid att köra bör exekveras på en bakgrundstråd (dvs inte på FX Application Thread).
Anledningen till den första regeln är att ramverket, som de flesta UI-verktygssatser, är skrivet utan någon synkronisering av tillståndet för element i scendiagrammet. Att lägga till synkronisering medför en prestationskostnad, och detta visar sig vara en oöverkomlig kostnad för UI-verktygssatser. Således kan endast en tråd säkert komma åt detta tillstånd. Eftersom UI-tråden (FX Application Thread for JavaFX) behöver komma åt detta tillstånd för att rendera scenen, är FX Application Thread den enda tråden där du kan komma åt "live" scengraftillstånd. I JavaFX 8 och senare utför de flesta metoder som omfattas av denna regel kontroller och ger körtidsundantag om regeln överträds. (Detta är i motsats till Swing, där du kan skriva "olaglig" kod och den kan se ut att fungera bra, men är i själva verket utsatt för slumpmässiga och oförutsägbara fel vid godtycklig tidpunkt.) Detta är orsaken till IllegalStateException
du ser :du anropar courseCodeLbl.setText(...)
från en annan tråd än FX Application Thread.
Anledningen till den andra regeln är att FX Application Thread, förutom att vara ansvarig för att bearbeta användarhändelser, också är ansvarig för att rendera scenen. Så om du utför en långvarig operation på den tråden kommer användargränssnittet inte att renderas förrän den operationen är klar, och kommer att inte svara på användarhändelser. Även om detta inte genererar undantag eller orsakar korrupt objekttillstånd (som överträdelse av regel 1 kommer), skapar det (i bästa fall) en dålig användarupplevelse.
Så om du har en långvarig operation (som att komma åt en databas) som behöver uppdatera användargränssnittet när det är klart, är grundplanen att utföra den långvariga operationen i en bakgrundstråd och returnera resultatet av operationen när den är slutför och schemalägg sedan en uppdatering av användargränssnittet i tråden för användargränssnittet (FX Application). Alla enkeltrådade UI-verktygssatser har en mekanism för att göra detta:i JavaFX kan du göra det genom att anropa Platform.runLater(Runnable r)
för att köra r.run()
på FX Application Thread. (I Swing kan du anropa SwingUtilities.invokeLater(Runnable r)
för att köra r.run()
på AWT-händelseutskickstråden.) JavaFX (se längre fram i det här svaret) tillhandahåller också ett API på högre nivå för att hantera kommunikationen tillbaka till FX Application Thread.
Allmän god praxis för multitrådning
Den bästa praxisen för att arbeta med flera trådar är att strukturera kod som ska exekveras på en "användardefinierad" tråd som ett objekt som är initierat med något fixerat tillstånd, har en metod för att utföra operationen och vid slutförande returnerar ett objekt representerar resultatet. Att använda oföränderliga objekt för det initierade tillståndet och beräkningsresultatet är mycket önskvärt. Tanken här är att eliminera möjligheten att alla föränderliga tillstånd är synliga från flera trådar så långt som möjligt. Att komma åt data från en databas passar detta formspråk bra:du kan initiera ditt "arbetar"-objekt med parametrarna för databasåtkomst (söktermer, etc). Utför databasfrågan och få en resultatuppsättning, använd resultatuppsättningen för att fylla i en samling domänobjekt och returnera samlingen i slutet.
I vissa fall kommer det att vara nödvändigt att dela föränderligt tillstånd mellan flera trådar. När detta absolut måste göras, måste du noggrant synkronisera åtkomsten till det tillståndet för att undvika att observera tillståndet i ett inkonsekvent tillstånd (det finns andra mer subtila frågor som måste åtgärdas, såsom tillståndets livlighet, etc). Den starka rekommendationen när detta behövs är att använda ett högnivåbibliotek för att hantera dessa komplexiteter åt dig.
Använda javafx.concurrent API
JavaFX tillhandahåller ett concurrency API
som är designad för att exekvera kod i en bakgrundstråd, med API speciellt designat för att uppdatera JavaFX UI vid slutförandet av (eller under) exekveringen av den koden. Detta API är utformat för att interagera med java.util.concurrent
API
, som tillhandahåller allmänna faciliteter för att skriva flertrådad kod (men utan UI-krokar). Nyckelklassen i javafx.concurrent
är Uppgift
, som representerar en enda engångsenhet av arbete som är avsedd att utföras på en bakgrundstråd. Den här klassen definierar en enda abstrakt metod, call()
, som inte tar några parametrar, returnerar ett resultat och kan skapa markerade undantag. Uppgift
implementerar Körbar
med dess run()
metod som helt enkelt anropar call()
. Uppgift
har också en samling metoder som garanterat uppdaterar tillståndet på FX Application Thread, såsom updateProgress(...)
, updateMessage(...)
, etc. Den definierar några observerbara egenskaper (t.ex. tillstånd
och värde
):lyssnare på dessa egenskaper kommer att meddelas om ändringar i FX Application Thread. Slutligen finns det några bekvämlighetsmetoder för att registrera hanterare (setOnSucceeded(...)
, setOnFailed(...)
, etc); alla hanterare som registrerats via dessa metoder kommer också att anropas på FX Application Thread.
Så den allmänna formeln för att hämta data från en databas är:
- Skapa en
uppgift
för att hantera anropet till databasen. - Initiera
uppgiften
med alla tillstånd som behövs för att utföra databasanropet. - Implementera uppgiftens
call()
metod för att utföra databasanropet och returnera resultatet av anropet. - Registrera en hanterare med uppgiften att skicka resultaten till användargränssnittet när det är klart.
- Anropa uppgiften i en bakgrundstråd.
För databasåtkomst rekommenderar jag starkt att kapsla in den faktiska databaskoden i en separat klass som inte vet något om användargränssnittet ( Designmönster för dataåtkomstobjekt ). Låt sedan uppgiften anropa metoderna på dataåtkomstobjektet.
Så du kanske har en DAO-klass som denna (observera att det inte finns någon UI-kod här):
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();
}
}
Att hämta ett gäng widgets kan ta lång tid, så alla anrop från en UI-klass (t.ex. en kontrollklass) bör schemalägga detta i en bakgrundstråd. En kontrollklass kan se ut så här:
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);
}
// ...
}
Lägg märke till hur anropet till den (potentiellt) långvariga DAO-metoden är insvept i en Task
som körs på en bakgrundstråd (via accessorn) för att förhindra blockering av användargränssnittet (regel 2 ovan). Uppdateringen av användargränssnittet (widgetTable.setItems(...)
) exekveras faktiskt tillbaka på FX Application Thread, med hjälp av Task
s bekvämlighetsåteruppringningsmetod setOnSucceeded(...)
(uppfyller regel 1).
I ditt fall returnerar databasåtkomsten du utför ett enda resultat, så du kan ha en metod som
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 ;
}
}
}
// ...
}
Och sedan skulle din kontrollkod se ut
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);
API-dokument för Task
har många fler exempel, inklusive uppdatering av förloppet
egenskapen för uppgiften (användbar för förloppsindikatorer... osv.