Dasar Thread

Membuat thread dengan Thread dan Runnable, memahami siklus hidupnya, serta mengenal thread daemon.

Daftar isi

Setiap program Java sesungguhnya sudah berjalan dengan minimal satu thread, yaitu main yang mengeksekusi metode main. Thread adalah alur eksekusi terkecil yang dijadwalkan JVM, dan pada mesin dengan beberapa inti prosesor, dua thread dapat berjalan benar-benar bersamaan. Kemampuan menambah thread inilah fondasi seluruh konkurensi di Java. Topik ini membahas dua cara membuat thread, siklus hidupnya dari NEW sampai TERMINATED, serta thread daemon yang bekerja di latar belakang tanpa menahan program tetap hidup.

Menjalankan tugas dengan Runnable dan Thread#

Cara yang direkomendasikan untuk membuat thread adalah menuliskan tugas sebagai Runnable, yaitu interface dengan satu metode run(). Objek Runnable kemudian diberikan kepada Thread, lengkap dengan nama agar mudah dilacak pada log. Pemanggilan start() meminta JVM menjalankan run() pada thread yang baru, bukan pada thread pemanggil, sedangkan join() membuat thread pemanggil menunggu sampai tugasnya selesai.

java DemoThreadDasar.java
public class DemoThreadDasar {

    public static void main(String[] args) throws InterruptedException {
        long[] hasil = new long[2];

        Thread pembaca = new Thread(() -> hasil[0] = jumlah(1, 500), "pembaca-berkas");
        Thread pengolah = new Thread(() -> hasil[1] = jumlah(501, 1000), "pengolah-data");

        pembaca.start();
        pengolah.start();

        System.out.println("main bebas mengerjakan hal lain selama tugas berjalan");

        pembaca.join();
        pengolah.join();

        System.out.println("pembaca-berkas : " + hasil[0]);
        System.out.println("pengolah-data  : " + hasil[1]);
        System.out.println("total          : " + (hasil[0] + hasil[1]));
    }

    static long jumlah(int awal, int akhir) {
        long total = 0;
        for (int i = awal; i <= akhir; i++) {
            total += i;
        }
        return total;
    }
}
text Keluaran
main bebas mengerjakan hal lain selama tugas berjalan
pembaca-berkas : 125250
pengolah-data  : 375250
total          : 500500

Pekerjaan dipecah menjadi dua bagian yang dihitung dua thread berbeda, lalu hasilnya digabung kembali oleh main. Susunan programnya disengaja agar keluarannya selalu sama, yaitu kedua thread tidak mencetak apa pun dan hanya menulis hasilnya ke array, sedangkan semua pencetakan dilakukan main setelah join(). Pada program nyata, urutan aktivitas antar-thread tidak terjamin, dan itulah persoalan yang ditangani topik-topik tentang sinkronisasi.

Cara kedua adalah mewarisi Thread dan menggantikan metode run()-nya. Bentuk ini tetap sah, tetapi jarang dipakai karena menyita satu-satunya slot pewarisan kelas untuk tugas yang sebenarnya bukan “sejenis thread”, melainkan “pekerjaan yang ingin dijalankan”.

java Contoh: alternatif mewarisi Thread
// tidak-dijalankan
class Pekerja extends Thread {
    @Override
    public void run() {
        System.out.println(getName() + " berjalan");
    }
}

// pemakaiannya:
Thread t = new Pekerja();
t.start();

Siklus hidup thread#

Thread bergerak melalui keadaan-keadaan yang didefinisikan enum Thread.State. Keadaan utamanya adalah NEW sebelum start(), RUNNABLE ketika siap atau sedang dieksekusi, WAITING dan TIMED_WAITING ketika menunggu sesuatu, BLOCKED ketika menunggu kunci yang dipegang thread lain, dan TERMINATED setelah run() selesai. Diagram berikut merangkum perpindahan antarkeadaan itu.

Siklus hidup thread JavaNEWbelum start()RUNNABLEsiap atau sedang berjalanTERMINATEDrun() selesaiWAITING / TIMED_WAITING/ BLOCKEDstart()run() selesaisleep(), wait(),menunggu kunciwaktunya tiba,notify(), monitor bebas
Siklus hidup thread Java dari NEW sampai TERMINATED

Program berikut mengamati keadaan yang sama pada satu thread, yaitu sebelum start(), saat run() sedang berjalan, dan setelah join().

java DemoSiklus.java
public class DemoSiklus {

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(
                () -> System.out.println("Di dalam run : " + Thread.currentThread().getState()));

        System.out.println("Sebelum start: " + t.getState());
        t.start();
        t.join();
        System.out.println("Setelah join : " + t.getState());
    }
}
text Keluaran
Sebelum start: NEW
Di dalam run : RUNNABLE
Setelah join : TERMINATED

Pembacaan pertama menghasilkan NEW karena start() belum dipanggil, sehingga objek Thread sudah ada tetapi belum dieksekusi. Baris tengah dicetak oleh thread itu sendiri ketika run() sedang berjalan, dan keadaan yang berjalan pasti RUNNABLE. Setelah join() kembali, tugasnya sudah selesai sehingga keadaannya TERMINATED. Keadaan BLOCKED, WAITING, dan TIMED_WAITING tidak muncul pada demo ini karena ketiganya butuh keadaan tunggu yang sengaja dibangun, misalnya sleep() atau perebutan kunci pada topik sinkronisasi.

Thread daemon#

JVM membedakan dua jenis thread, yaitu thread biasa (user) dan thread daemon. Aturannya satu kalimat, JVM berhenti begitu tidak ada lagi thread biasa yang hidup, dan semua daemon yang tersisa langsung ditinggalkan. Thread daemon cocok untuk pekerjaan pemantau, pengingat, atau pembersihan yang tidak boleh menahan program tetap hidup, dan garbage collector sendiri berjalan sebagai daemon. Penandaannya lewat setDaemon(true) yang wajib dipanggil sebelum start().

java DemoDaemon.java
public class DemoDaemon {

    public static void main(String[] args) throws InterruptedException {
        Thread pengingat = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });
        pengingat.setDaemon(true);
        pengingat.start();
        System.out.println("pengingat daemon? " + pengingat.isDaemon());

        Thread pekerja = new Thread(() -> {
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        pekerja.start();
        pekerja.join();

        System.out.println("pekerja selesai, main berakhir meski pengingat masih hidup");
    }
}
text Keluaran
pengingat daemon? true
pekerja selesai, main berakhir meski pengingat masih hidup

Thread pengingat dirancang berputar tanpa akhir, tetapi setelah main dan pekerja selesai, tidak ada thread biasa yang tersisa sehingga JVM langsung menutup program beserta daemonnya. Perilaku ini penting dipahami sebelum menandai sebuah thread sebagai daemon, karena pekerjaan yang belum selesai pada daemon bisa terpotong di tengah jalan.

Kesalahan umum#

  • Memanggil run() langsung alih-alih start(). Metode run() yang dipanggil langsung tereksekusi pada thread pemanggil, sehingga tidak ada konkurensi sama sekali. Gunakan start() untuk meminta thread baru.
  • Membaca hasil pekerjaan thread lain tanpa join(). main bisa membaca data sebelum thread yang menulisnya selesai bekerja. Panggil join() sebelum hasilnya dihitung atau dicetak.
  • Memanggil setDaemon(true) setelah start(). Penandaan hanya sah saat thread masih NEW, dan pelanggarannya melempar IllegalThreadStateException.
  • Menyusun logika dengan asumsi urutan antar-thread. Penjadwal sistem operasi yang menentukan urutan eksekusinya, sehingga satu-satunya cara menyamakan langkah adalah join() atau mekanisme koordinasi lainnya.

Ringkasan#

  • Tugas dibungkus sebagai Runnable lalu dijalankan Thread lewat start(), dan thread pemanggil menunggu selesainya lewat join().
  • Siklus hidup thread bergerak dari NEW, RUNNABLE, kemungkinan keadaan tunggu BLOCKED, WAITING, atau TIMED_WAITING, hingga TERMINATED, dan dapat dibaca lewat getState().
  • Mewarisi Thread sah tetapi jarang dipakai karena menyita slot pewarisan kelas.
  • Thread daemon tidak menahan JVM tetap hidup, dan setDaemon(true) wajib dipanggil sebelum start().

Referensi#