Antarmuka yang responsif punya satu syarat tak tertulis: thread yang menggambar dan menangani klik harus selalu bebas. Begitu kita menjalankan pekerjaan berat — unduhan berkas, kueri database, komputasi besar — pada thread UI, seluruh jendela berhenti merespons: tombol tidak memerah saat disentuh, jendela tidak bisa digeser, dan sistem operasi menampilkan indikasi “tidak merespons”. Penyebabnya bukan kelemahan toolkit, melainkan arsitektur satu-antrean yang sudah kita kenal: kejadian pengguna dan kode pekerjaan berat kita antre di thread yang sama. Solusinya baku di semua toolkit UI: kerjakan di thread latar, laporkan hasil kembali ke thread UI. JavaFX menyediakan Task dan Service untuk itu; Swing menyediakan SwingWorker. Chapter ini membedah ketiganya lewat satu studi kasus unduhan bertahap.

Gejala Aplikasi Membeku #

Thread JavaFX Application Thread menjalankan loop yang sama dengan EDT: ambil satu kejadian, proses, ulangi. Ketika handler tombol berisi Thread.sleep(5000) atau kueri lambat, loop itu tertahan di dalam handler kita — kejadian klik berikutnya menumpuk di antrean tanpa pernah diambil. Berikut pola keliru yang paling sering ditiru pemula:

java PolaBeku.java — tampilan pola keliru
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class PolaBeku extends Application {
    @Override
    public void start(Stage panggung) {
        Label status = new Label("Siap");
        Button muat = new Button("Muat data");

        // KELIRU: pekerjaan berat langsung di thread UI
        muat.setOnAction(e -> {
            status.setText("Memuat...");
            try {
                Thread.sleep(5000);            // jendela beku 5 detik
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
            }
            status.setText("Selesai");
        });

        panggung.setScene(new Scene(new VBox(10, status, muat), 320, 160));
        panggung.show();
    }
}

Jebakannya halus: status.setText("Memuat...") sering tidak tampak sebelum jeda — teks itu memang sudah disimpan, namun pengecatan ulang layar juga dijadwalkan di thread yang sedang tertidur. Label langsung melompat ke “Selesai” lima detik kemudian.

Task Pekerjaan Sekali Jalan #

javafx.concurrent.Task<T> adalah Worker sekali pakai untuk satu unit pekerjaan. Kita menulis kode berat di call(), yang berjalan di thread latar milik executor internal; kemudian updateProgress, updateMessage, dan updateValue mengantarkan laporan ke JavaFX Application Thread dengan aman:

java TugasUnduh.java — tampilan Task JavaFX
import javafx.concurrent.Task;

public class TugasUnduh extends Task<String> {
    private final int totalPotongan;

    public TugasUnduh(int totalPotongan) {
        this.totalPotongan = totalPotongan;
    }

    @Override
    protected String call() throws Exception {
        int terunduh = 0;
        for (int i = 1; i <= totalPotongan; i++) {
            Thread.sleep(200);              // simulasi jaringan
            terunduh += 100 / totalPotongan;
            updateProgress(terunduh, 100);  // aman dipanggil dari thread latar
            updateMessage("Potongan " + i + " selesai");
            if (isCancelled()) {
                return "dibatalkan";
            }
        }
        return "selesai, " + terunduh + " persen";
    }
}

Method updateProgress(m, n) dan updateMessage(...) tidak menyetel properti dari thread latar. Keduanya hanya mengantrekan pembaruan; mesin Task yang memindahkan nilai ke thread JavaFX dan baru di situ menyetel progressProperty serta messageProperty — properti yang bisa kita ikat langsung ke ProgressBar dan Label tanpa satu baris kode antara. Pemakaian task di aplikasi:

java MemakaiTask.java — tampilan pemakaian
TugasUnduh tugas = new TugasUnduh(5);
ProgressBar bar = new ProgressBar();
bar.progressProperty().bind(tugas.progressProperty());
Label status = new Label();
status.textProperty().bind(tugas.messageProperty());

new Thread(tugas).start(); // Task adalah Runnable

Pengikatan properti pada potongan di atas memperlihatkan kemenangan gabungan dua chapter: property dari Chapter sebelumnya bertemu concurrency chapter ini — ProgressBar memperbarui dirinya sendiri setiap updateProgress tiba.

Service untuk Pekerjaan Berulang #

Task sekali pakai: setelah selesai atau gagal, objek yang sama tidak boleh dijalankan lagi. Untuk pekerjaan yang dipicu berulang — tombol “muat ulang” yang boleh ditekan kapan saja — JavaFX menyediakan Service<T>: pembungkus yang membuat Task baru setiap kali restart() dipanggil, sambil menjaga properti stateProperty, progressProperty, dan messageProperty tetap sama sehingga binding UI tidak perlu dibongkar. Hubungan ketiganya: Service adalah pabrik Task; Task adalah pekerjaan; keduanya mengimplementasikan antarmuka Worker yang menyatukan properti status.

SwingWorker Saudara Swing #

Mekanisme yang sama sudah hadir di Swing sejak 2006 dengan nama SwingWorker<T, V>: doInBackground() berjalan di thread latar, process(List<V>) menerima kiriman publish(...) di EDT, dan done() dijalankan di EDT setelah selesai. Karena SwingWorker tinggal di modul java.desktop milik JDK, kita bisa menguji perilakunya secara runnable — lengkap dengan bukti bahwa process memang berjalan di EDT:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;

/** SwingWorker: pekerjaan berat di thread latar, notifikasi di EDT. */
public class PekerjaLatar {
    static final int[] BEBAN = { 10, 15, 25, 25, 15 };

    public static void main(String[] args) throws Exception {
        List<Integer> progres = Collections.synchronizedList(new ArrayList<>());
        List<Boolean> prosesDiEdt = Collections.synchronizedList(new ArrayList<>());

        SwingWorker<String, Integer> pekerja = new SwingWorker<>() {
            @Override
            protected String doInBackground() {
                int total = 0;
                for (int b : BEBAN) {
                    try {
                        Thread.sleep(20); // simulasi unduhan satu potongan
                    } catch (InterruptedException e) {
                        return "terputus";
                    }
                    total += b;
                    publish(total); // kirim progres ke EDT
                }
                return "selesai, total beban " + total;
            }

            @Override
            protected void process(List<Integer> potongan) {
                // berjalan di EDT: satu-satunya tempat yang boleh menyentuh UI
                prosesDiEdt.add(SwingUtilities.isEventDispatchThread());
                progres.addAll(potongan);
            }
        };

        pekerja.execute();            // mulai pekerjaan di thread latar
        String hasil = pekerja.get(); // tunggu pekerjaan tuntas

        // Tunggu seluruh potongan progres tiba di EDT (paling lama 2 detik).
        for (int i = 0; i < 400 && progres.size() < BEBAN.length; i++) {
            Thread.sleep(5);
        }

        System.out.println("hasil                = " + hasil);
        System.out.println("urutan progres       = " + progres);
        System.out.println("process selalu di EDT = " + !prosesDiEdt.contains(false));
    }
}

Output yang Dihasilkan:

text
hasil                = selesai, total beban 90
urutan progres       = [10, 25, 50, 75, 90]
process selalu di EDT = true

Mari kita bedah bagian-bagian pentingnya:

  1. Baris 12-13 (synchronizedList): doInBackground (thread latar) dan process (EDT) menulis ke daftar yang sama, jadi daftar perlu pembungkus yang menyinkronkan tiap akses. Ini satu-satunya titik tempat dua thread bertemu di program ini.
  2. Baris 16 (new SwingWorker<String, Integer>): parameter pertama adalah tipe hasil doInBackground; parameter kedua adalah tipe kiriman publish. Program kita mengembalikan String dan mengirim Integer progres.
  3. Baris 27 (publish(total)): dipanggil dari thread latar sebanyak lima kali. Swing tidak mengantarkan tiap kiriman satu per satu — kiriman yang berdekatan digabung menjadi satu panggilan process, itulah sebabnya process menerima List<Integer>, bukan satu nilai.
  4. Baris 33-36 (process di EDT): SwingUtilities.isEventDispatchThread() diperiksa setiap kali dan hasilnya (selalu true) dikumpulkan — bukti bahwa seluruh notifikasi memang dikirim ke EDT meski publish dipanggil dari thread latar.
  5. Baris 44 (pekerja.get()): method ini memblokir thread pemanggil sampai doInBackground selesai dan mengembalikan hasilnya — atau melempar ExecutionException bila pekerjaan gagal. Pada aplikasi nyata, pembaruan UI justru dilakukan di done() (yang berjalan di EDT), bukan lewat get() dari thread sembarangan.
  6. Baris 47-49 (loop tunggu): get() kembali saat pekerjaan selesai, tetapi kiriman process terakhir bisa saja masih mengantre di EDT. Loop kecil ini menunggu hingga kelima potongan progres tiba sebelum mencetak, sehingga keluaran selalu utuh.

Satu Pola di Mana-mana #

Keluarga kelas ini lahir dari kebutuhan yang sama sehingga strukturnya hampir saling menempel:

PeranSwingJavaFX
Unit pekerjaanSwingWorker.doInBackground()Task.call()
Laporan bertahappublish → processupdateProgress/updateValue
Selesai di thread UIdone()succeeded() / onSucceeded
Berulangbuat SwingWorker baruService.restart()
Status yang bisa diikat— (getter + listener)Worker.stateProperty()

Dengan bekal ini, aplikasi desktop kita lengkap secara teknik: jendela, data, reaksi, dan pekerjaan latar. Satu langkah terakhir memisahkan proyek belajar dari produk nyata — mengemas aplikasi menjadi pemasang yang bisa dijalankan pengguna tanpa JDK. Chapter berikutnya membahas jlink, jdeps, dan jpackage.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

PekerjaLatar.java

Unduh berkas
java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;

/** SwingWorker: pekerjaan berat di thread latar, notifikasi di EDT. */
public class PekerjaLatar {
    static final int[] BEBAN = { 10, 15, 25, 25, 15 };

    public static void main(String[] args) throws Exception {
        List<Integer> progres = Collections.synchronizedList(new ArrayList<>());
        List<Boolean> prosesDiEdt = Collections.synchronizedList(new ArrayList<>());

        SwingWorker<String, Integer> pekerja = new SwingWorker<>() {
            @Override
            protected String doInBackground() {
                int total = 0;
                for (int b : BEBAN) {
                    try {
                        Thread.sleep(20); // simulasi unduhan satu potongan
                    } catch (InterruptedException e) {
                        return "terputus";
                    }
                    total += b;
                    publish(total); // kirim progres ke EDT
                }
                return "selesai, total beban " + total;
            }

            @Override
            protected void process(List<Integer> potongan) {
                // berjalan di EDT: satu-satunya tempat yang boleh menyentuh UI
                prosesDiEdt.add(SwingUtilities.isEventDispatchThread());
                progres.addAll(potongan);
            }
        };

        pekerja.execute();            // mulai pekerjaan di thread latar
        String hasil = pekerja.get(); // tunggu pekerjaan tuntas

        // Tunggu seluruh potongan progres tiba di EDT (paling lama 2 detik).
        for (int i = 0; i < 400 && progres.size() < BEBAN.length; i++) {
            Thread.sleep(5);
        }

        System.out.println("hasil                = " + hasil);
        System.out.println("urutan progres       = " + progres);
        System.out.println("process selalu di EDT = " + !prosesDiEdt.contains(false));
    }
}