Aturan Threading untuk JavaFX
Ada dua aturan dasar untuk utas dan JavaFX:
- Kode apa pun yang mengubah atau mengakses status simpul yang merupakan bagian dari grafik adegan harus dieksekusi pada utas aplikasi JavaFX. Operasi tertentu lainnya (mis. membuat
Stage
baru s) juga terikat oleh aturan ini. - Kode apa pun yang mungkin membutuhkan waktu lama untuk dijalankan harus dieksekusi di utas latar belakang (yaitu bukan di Utas Aplikasi FX).
Alasan untuk aturan pertama adalah, seperti kebanyakan toolkit UI, framework ditulis tanpa sinkronisasi pada status elemen grafik adegan. Menambahkan sinkronisasi menimbulkan biaya kinerja, dan ini ternyata menjadi biaya yang mahal untuk toolkit UI. Jadi hanya satu utas yang dapat mengakses status ini dengan aman. Karena utas UI (Utas Aplikasi FX untuk JavaFX) perlu mengakses status ini untuk merender adegan, Utas Aplikasi FX adalah satu-satunya utas tempat Anda dapat mengakses status grafik adegan "langsung". Di JavaFX 8 dan yang lebih baru, sebagian besar metode yang tunduk pada aturan ini melakukan pemeriksaan dan membuang pengecualian runtime jika aturan dilanggar. (Hal ini berbeda dengan Swing, di mana Anda dapat menulis kode "ilegal" dan mungkin tampak berjalan dengan baik, tetapi sebenarnya rentan terhadap kegagalan acak dan tak terduga pada waktu yang sewenang-wenang.) Inilah penyebab dari IllegalStateException
Anda melihat :Anda memanggil courseCodeLbl.setText(...)
dari utas selain Utas Aplikasi FX.
Alasan untuk aturan kedua adalah bahwa FX Application Thread, selain bertanggung jawab untuk memproses peristiwa pengguna, juga bertanggung jawab untuk merender adegan. Jadi, jika Anda melakukan operasi yang berjalan lama pada utas itu, UI tidak akan dirender hingga operasi itu selesai, dan akan menjadi tidak responsif terhadap peristiwa pengguna. Meskipun ini tidak akan menghasilkan pengecualian atau menyebabkan status objek yang rusak (seperti yang akan terjadi pada pelanggaran aturan 1), ini (paling baik) menciptakan pengalaman pengguna yang buruk.
Jadi, jika Anda memiliki operasi yang berjalan lama (seperti mengakses database) yang perlu memperbarui UI setelah selesai, rencana dasarnya adalah melakukan operasi yang berjalan lama di utas latar belakang, mengembalikan hasil operasi saat itu selesai, lalu jadwalkan pembaruan ke UI di utas UI (Aplikasi FX). Semua toolkit UI single-threaded memiliki mekanisme untuk melakukan ini:di JavaFX Anda dapat melakukannya dengan memanggil Platform.runLater(Runnable r)
untuk mengeksekusi r.run()
di Utas Aplikasi FX. (Di Swing, Anda dapat memanggil SwingUtilities.invokeLater(Runnable r)
untuk mengeksekusi r.run()
pada utas pengiriman peristiwa AWT.) JavaFX (lihat nanti dalam jawaban ini) juga menyediakan beberapa API tingkat yang lebih tinggi untuk mengelola komunikasi kembali ke Utas Aplikasi FX.
Praktik Umum yang Baik untuk Multithreading
Praktik terbaik untuk bekerja dengan banyak utas adalah menyusun kode yang akan dieksekusi pada utas "ditentukan pengguna" sebagai objek yang diinisialisasi dengan beberapa status tetap, memiliki metode untuk melakukan operasi, dan setelah selesai mengembalikan objek mewakili hasilnya. Menggunakan objek yang tidak dapat diubah untuk keadaan yang diinisialisasi dan hasil komputasi sangat diinginkan. Idenya di sini adalah untuk menghilangkan kemungkinan keadaan yang bisa berubah yang terlihat dari banyak utas sejauh mungkin. Mengakses data dari database cocok dengan idiom ini:Anda dapat menginisialisasi objek "pekerja" Anda dengan parameter untuk akses database (istilah pencarian, dll). Lakukan kueri database dan dapatkan kumpulan hasil, gunakan kumpulan hasil untuk mengisi kumpulan objek domain, dan kembalikan kumpulan di akhir.
Dalam beberapa kasus, akan diperlukan untuk berbagi status yang dapat berubah di antara banyak utas. Ketika ini benar-benar harus dilakukan, Anda perlu menyinkronkan akses dengan hati-hati ke keadaan itu untuk menghindari mengamati keadaan dalam keadaan yang tidak konsisten (ada masalah lain yang lebih halus yang perlu ditangani, seperti kehidupan keadaan, dll). Rekomendasi kuat saat ini diperlukan adalah menggunakan perpustakaan tingkat tinggi untuk mengelola kerumitan ini untuk Anda.
Menggunakan javafx.concurrent API
JavaFX menyediakan API konkurensi
yang dirancang untuk mengeksekusi kode di utas latar belakang, dengan API yang dirancang khusus untuk memperbarui UI JavaFX pada penyelesaian (atau selama) eksekusi kode tersebut. API ini dirancang untuk berinteraksi dengan java.util.concurrent
API
, yang menyediakan fasilitas umum untuk menulis kode multithread (tetapi tanpa kait UI). Kelas kunci di javafx.concurrent
adalah Task
, yang merepresentasikan satu unit pekerjaan sekali pakai yang dimaksudkan untuk dilakukan pada utas latar belakang. Kelas ini mendefinisikan satu metode abstrak, call()
, yang tidak mengambil parameter, mengembalikan hasil, dan mungkin membuang pengecualian yang dicentang. Task
mengimplementasikan Runnable
dengan run()
metode cukup memanggil call()
. Task
juga memiliki kumpulan metode yang dijamin untuk memperbarui status di Utas Aplikasi FX, seperti updateProgress(...)
, updateMessage(...)
, dll. Ini mendefinisikan beberapa properti yang dapat diamati (mis. state
dan value
):pendengar properti ini akan diberitahu tentang perubahan pada Thread Aplikasi FX. Terakhir, ada beberapa metode praktis untuk mendaftarkan penangan (setOnSucceeded(...)
, setOnFailed(...)
, dll); penangan apa pun yang terdaftar melalui metode ini juga akan dipanggil di Thread Aplikasi FX.
Jadi rumus umum untuk mengambil data dari database adalah:
- Buat
Task
untuk menangani panggilan ke database. - Inisialisasi
Task
dengan status apa pun yang diperlukan untuk melakukan panggilan database. - Implementasikan
call()
task tugas metode untuk melakukan panggilan database, mengembalikan hasil panggilan. - Daftarkan penangan dengan tugas untuk mengirim hasilnya ke UI setelah selesai.
- Ajukan tugas di utas latar belakang.
Untuk akses basis data, saya sangat menyarankan enkapsulasi kode basis data aktual di kelas terpisah yang tidak tahu apa-apa tentang UI ( Pola desain Objek Akses Data ). Kemudian tugas memanggil metode pada objek akses data.
Jadi Anda mungkin memiliki kelas DAO seperti ini (perhatikan tidak ada kode UI di sini):
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();
}
}
Mengambil banyak widget mungkin membutuhkan waktu lama, jadi panggilan apa pun dari kelas UI (misalnya kelas pengontrol) harus menjadwalkannya di utas latar belakang. Kelas pengontrol mungkin terlihat seperti ini:
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);
}
// ...
}
Perhatikan bagaimana panggilan ke metode DAO yang (berpotensi) berjalan lama dibungkus dalam Task
yang dijalankan pada utas latar belakang (melalui pengakses) untuk mencegah pemblokiran UI (aturan 2 di atas). Pembaruan pada UI (widgetTable.setItems(...)
) sebenarnya dieksekusi kembali di Utas Aplikasi FX, menggunakan Task
metode panggilan balik kenyamanan setOnSucceeded(...)
(memuaskan aturan 1).
Dalam kasus Anda, akses database yang Anda lakukan mengembalikan satu hasil, jadi Anda mungkin memiliki metode seperti
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 ;
}
}
}
// ...
}
Dan kemudian kode pengontrol Anda akan terlihat seperti
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);
dokumen API untuk Task
memiliki lebih banyak contoh, termasuk memperbarui progress
properti tugas (berguna untuk bilah kemajuan..., dll.