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.
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;
}
}main bebas mengerjakan hal lain selama tugas berjalan
pembaca-berkas : 125250
pengolah-data : 375250
total : 500500Pekerjaan 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”.
// 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.
Program berikut mengamati keadaan yang sama pada satu thread, yaitu sebelum start(), saat run() sedang berjalan, dan setelah join().
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());
}
}Sebelum start: NEW
Di dalam run : RUNNABLE
Setelah join : TERMINATEDPembacaan 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().
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");
}
}pengingat daemon? true
pekerja selesai, main berakhir meski pengingat masih hidupThread 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-alihstart(). Metoderun()yang dipanggil langsung tereksekusi pada thread pemanggil, sehingga tidak ada konkurensi sama sekali. Gunakanstart()untuk meminta thread baru. - Membaca hasil pekerjaan thread lain tanpa
join().mainbisa membaca data sebelum thread yang menulisnya selesai bekerja. Panggiljoin()sebelum hasilnya dihitung atau dicetak. - Memanggil
setDaemon(true)setelahstart(). Penandaan hanya sah saat thread masihNEW, dan pelanggarannya melemparIllegalThreadStateException. - 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
Runnablelalu dijalankanThreadlewatstart(), dan thread pemanggil menunggu selesainya lewatjoin(). - Siklus hidup thread bergerak dari
NEW,RUNNABLE, kemungkinan keadaan tungguBLOCKED,WAITING, atauTIMED_WAITING, hinggaTERMINATED, dan dapat dibaca lewatgetState(). - Mewarisi
Threadsah tetapi jarang dipakai karena menyita slot pewarisan kelas. - Thread daemon tidak menahan JVM tetap hidup, dan
setDaemon(true)wajib dipanggil sebelumstart().
Referensi#
- Class Thread — Oracle
— dokumentasi resmi kelas
Thread, termasukstart,join,getState, dansetDaemon. - Defining and Starting a Thread — Oracle
— tutorial resmi membuat thread lewat
Runnabledan pewarisanThread.