Setelah memahami apa itu proses dan thread, sekarang saatnya menciptakan thread pertama kita secara nyata. Java menyediakan dua jalur klasik: menurunkan class Thread, atau membuat class Runnable yang dibungkus objek Thread. Keduanya menghasilkan thread yang sama kuatnya, tetapi ada alasan praktis mengapa jalur Runnable lebih sering dipilih di kode profesional. Chapter ini juga membahas dua hal yang sering menjatuhkan pemula: perbedaan nyata antara start() dan run(), serta cara menghentikan thread secara kooperatif dengan interrupt().
Dua Jalur Membuat Thread #
Jalur pertama adalah subclass dari Thread lalu meng-override method run(). Jalur kedua adalah mengimplementasikan interface Runnable lalu membungkus objek tersebut ke dalam Thread. Program berikut mendemonstrasikan kedua jalur sekaligus — dua thread menghitung nilai berbeda, hasilnya dikumpulkan ke satu array, lalu dicetak setelah kedua thread selesai:
public class CaraMembuatThread {
public static void main(String[] args) throws InterruptedException {
int[] hasil = new int[2];
// Jalur 1: membuat subclass dari Thread.
Thread t1 = new PenghitungKuadrat(hasil, 0, 12);
t1.setName("kuadrat");
// Jalur 2: membuat class yang mengimplementasikan Runnable,
// lalu membungkusnya dengan objek Thread.
Thread t2 = new Thread(new PenghitungPangkatTiga(hasil, 1, 9));
t2.setName("pangkat-tiga");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("kuadrat dari 12 : " + hasil[0]);
System.out.println("pangkat tiga dari 9 : " + hasil[1]);
}
}
class PenghitungKuadrat extends Thread {
private final int[] hasil;
private final int indeks;
private final int nilai;
PenghitungKuadrat(int[] hasil, int indeks, int nilai) {
this.hasil = hasil;
this.indeks = indeks;
this.nilai = nilai;
}
@Override
public void run() {
hasil[indeks] = nilai * nilai;
}
}
class PenghitungPangkatTiga implements Runnable {
private final int[] hasil;
private final int indeks;
private final int nilai;
PenghitungPangkatTiga(int[] hasil, int indeks, int nilai) {
this.hasil = hasil;
this.indeks = indeks;
this.nilai = nilai;
}
@Override
public void run() {
hasil[indeks] = nilai * nilai * nilai;
}
}Output yang Dihasilkan:
kuadrat dari 12 : 144
pangkat tiga dari 9 : 729Mari kita telaah baris-baris kode di atas:
- Baris 4 (
int[] hasil = new int[2];): menyiapkan array di heap sebagai wadah hasil dari kedua thread. Slot indeks tetap mencegah kedua thread menulis ke lokasi yang sama. - Baris 7 (
Thread t1 = new PenghitungKuadrat(...)): objekPenghitungKuadratadalah sekaligus objekThreadkarena hubungan pewarisan, sehingga bisa langsung di-start(). - Baris 12 (
Thread t2 = new Thread(new PenghitungPangkatTiga(...))): objek tugas (Runnable) dipisahkan dari objek pelaksana (Thread). Tugas bisa juga dikirim ke thread pool — lihat Section tentangExecutorServicenanti. - Baris 17-18 (
t1.start()dant2.start()):start()meminta JVM membuat thread baru; JVM-lah yang kemudian memanggil methodrun()di thread tersebut. - Baris 19-20 (
t1.join()dant2.join()): threadmainmenunggu kedua pekerja menyelesaikanrun()sebelum melanjutkan ke pencetakan hasil.
Mengapa Runnable Lebih Fleksibel #
Java hanya mengizinkan single inheritance: class PenghitungKuadrat sudah “menghabiskan” kuota pewarisannya untuk Thread, sehingga tidak bisa lagi mewarisi class lain. Interface Runnable tidak menghabiskan kuota itu — class tugas bebas mewarisi class domain apa pun yang masuk akal, misalnya class LaporanPenjualan extends Dokumen implements Runnable.
Alasan kedua bersifat arsitektural: Runnable memisahkan apa yang dikerjakan dari siapa yang mengerjakan. Tugas berbentuk Runnable bisa dijalankan langsung oleh Thread, tetapi juga oleh ExecutorService, FutureTask, atau virtual thread tanpa perubahan kode. Satu bonus kecil: Runnable adalah functional interface, sehingga kita bisa menulis tugas langsung sebagai lambda () -> {...} seperti pada contoh-contoh berikutnya di Section ini.
start() vs run() yang Sering Menjebak #
Method run() hanyalah method biasa; tidak ada yang istimewa dari pemanggilannya. Hanya start() yang meminta JVM membuat thread baru. Program berikut membuktikan perbedaan tersebut dengan merekam nama thread yang mengeksekusi body tugas:
public class StartVsRun {
public static void main(String[] args) throws InterruptedException {
// Slot untuk merekam nama thread yang menjalankan body tugas.
String[] pelaksana = new String[1];
Thread t = new Thread(() -> {
// Thread.currentThread() mengembalikan thread yang sedang menjalankan
// baris ini, bisa thread main ataupun thread baru, tergantung siapa
// pemanggil run().
pelaksana[0] = Thread.currentThread().getName();
});
t.setName("pekerja");
// Eksperimen 1: memanggil run() secara langsung.
// Tidak ada thread baru; body dieksekusi sinkron oleh thread main.
t.run();
System.out.println("run() dieksekusi oleh : " + pelaksana[0]);
// Eksperimen 2: memanggil start().
// JVM membuat thread baru bernama "pekerja" dan menjalankan run() di sana.
t.start();
t.join();
System.out.println("start() dieksekusi oleh : " + pelaksana[0]);
}
}Output yang Dihasilkan:
run() dieksekusi oleh : main
start() dieksekusi oleh : pekerjaHasilnya membuktikan dua fakta. Pertama, t.run() dieksekusi oleh thread main — program berjalan sekuensial persis seperti memanggil method biasa, tidak ada paralelisme sama sekali. Kedua, t.start() membuat JVM mengeksekusi run() di thread baru bernama pekerja. Kode di atas juga sah dipanggil berurutan karena run() yang dipanggil manual tidak mengubah state thread; thread masih NEW dan boleh di-start().
Menunggu dengan join() #
Method join() membuat thread pemanggil menunggu hingga thread target selesai. Tanpa join(), thread main di contoh kita bisa mencetak hasil[0] sebelum pekerja sempat mengisinya — hasil tercetak bisa 0 karena array belum diisi. Ada pula varian join(long millis) yang menunggu maksimal selama durasi tertentu lalu melanjutkan eksekusi meski thread target belum selesai. Sepanjang Section ini kita akan memakai join() sebagai alat bantu agar keluaran contoh tetap terprediksi: kumpulkan hasil dulu, cetak setelah semua thread bergabung.
Memberi Nama pada Thread #
Setiap thread punya nama. Tanpa setName(), JVM memberi nama otomatis seperti Thread-0; nama khusus seperti "pekerja" atau "pembaca-cache" jauh lebih mudah dibaca saat kita memeriksa aplikasi yang sedang berjalan, misalnya lewat dump thread jstack atau panel thread di debugger. Gunakan setName() setelah membuat objek, atau constructor new Thread(runnable, "nama") untuk langsung menamai.
Menghentikan Thread secara Kooperatif #
Java modern tidak menyediakan tombol paksa untuk menghentikan thread — method Thread.stop() sudah lama dihapus dari praktik yang disarankan karena berbahaya: thread bisa berhenti di tengah operasi dan meninggalkan data dalam keadaan setengah jadi. Cara yang benar adalah kooperatif: interrupt() hanya mengirim sinyal permintaan berhenti, dan thread target yang memutuskan kapan serta cara berhenti:
public class InterupsiThread {
public static void main(String[] args) throws InterruptedException {
boolean[] berhenti = new boolean[1];
Thread pekerja = new Thread(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
// Simulasi pekerjaan yang bisa berhenti di tengah jalan.
Thread.sleep(10);
}
// Keluar dari loop karena flag interupsi aktif.
berhenti[0] = true;
} catch (InterruptedException e) {
// sleep() yang sedang berjalan dibatalkan oleh interrupt();
// kita menandai diri berhenti secara kooperatif.
berhenti[0] = true;
Thread.currentThread().interrupt(); // pulihkan flag untuk pemanggil berikutnya
}
});
pekerja.setName("pekerja");
pekerja.start();
Thread.sleep(50); // biarkan pekerja bekerja sesaat
pekerja.interrupt(); // kirim sinyal permintaan berhenti
pekerja.join(); // tunggu pekerja benar-benar selesai
System.out.println("pekerja berhenti secara kooperatif: " + berhenti[0]);
}
}Output yang Dihasilkan:
pekerja berhenti secara kooperatif: trueMari kita telaah mekanisme di baliknya:
- Baris 14 (
while (!Thread.currentThread().isInterrupted())): loop memeriksa flag interupsi setiap iterasi. Flag ini adalah komunikasi satu arah dari pemanggilinterrupt()ke thread ini. - Baris 22 (
catch (InterruptedException e)): method blocking sepertisleep(),join(), danwait()merespons interupsi dengan melemparInterruptedExceptionsekaligus menghapus flag interupsi. - Baris 25 (
Thread.currentThread().interrupt())): mengembalikan status interupsi yang barusan dihapus, sehingga kode pemanggil di atas tumpukan panggilan tetap tahu bahwa interupsi pernah terjadi. Kebiasaan ini penting bila method kita sendiri tidak memiliki cara lain menyampaikan sinyal tersebut.
Dua jalur pembuatan thread di atas memakai kontrak Runnable yang method run()-nya tidak mengembalikan nilai. Bagaimana bila tugas perlu mengembalikan hasil atau melempar checked exception? Chapter berikutnya memperkenalkan Callable dan jembatan FutureTask untuk kebutuhan itu.
Referensi #
- Oracle: Class Thread (Java API Specification)
- Oracle: Interface Runnable (Java API Specification)
- Oracle: Java Concurrency (dev.java)
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
CaraMembuatThread.java
Unduh berkaspublic class CaraMembuatThread {
public static void main(String[] args) throws InterruptedException {
int[] hasil = new int[2];
// Jalur 1: membuat subclass dari Thread.
Thread t1 = new PenghitungKuadrat(hasil, 0, 12);
t1.setName("kuadrat");
// Jalur 2: membuat class yang mengimplementasikan Runnable,
// lalu membungkusnya dengan objek Thread.
Thread t2 = new Thread(new PenghitungPangkatTiga(hasil, 1, 9));
t2.setName("pangkat-tiga");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("kuadrat dari 12 : " + hasil[0]);
System.out.println("pangkat tiga dari 9 : " + hasil[1]);
}
}
class PenghitungKuadrat extends Thread {
private final int[] hasil;
private final int indeks;
private final int nilai;
PenghitungKuadrat(int[] hasil, int indeks, int nilai) {
this.hasil = hasil;
this.indeks = indeks;
this.nilai = nilai;
}
@Override
public void run() {
hasil[indeks] = nilai * nilai;
}
}
class PenghitungPangkatTiga implements Runnable {
private final int[] hasil;
private final int indeks;
private final int nilai;
PenghitungPangkatTiga(int[] hasil, int indeks, int nilai) {
this.hasil = hasil;
this.indeks = indeks;
this.nilai = nilai;
}
@Override
public void run() {
hasil[indeks] = nilai * nilai * nilai;
}
}
InterupsiThread.java
Unduh berkaspublic class InterupsiThread {
public static void main(String[] args) throws InterruptedException {
boolean[] berhenti = new boolean[1];
Thread pekerja = new Thread(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
// Simulasi pekerjaan yang bisa berhenti di tengah jalan.
Thread.sleep(10);
}
// Keluar dari loop karena flag interupsi aktif.
berhenti[0] = true;
} catch (InterruptedException e) {
// sleep() yang sedang berjalan dibatalkan oleh interrupt();
// kita menandai diri berhenti secara kooperatif.
berhenti[0] = true;
Thread.currentThread().interrupt(); // pulihkan flag untuk pemanggil berikutnya
}
});
pekerja.setName("pekerja");
pekerja.start();
Thread.sleep(50); // biarkan pekerja bekerja sesaat
pekerja.interrupt(); // kirim sinyal permintaan berhenti
pekerja.join(); // tunggu pekerja benar-benar selesai
System.out.println("pekerja berhenti secara kooperatif: " + berhenti[0]);
}
}
StartVsRun.java
Unduh berkaspublic class StartVsRun {
public static void main(String[] args) throws InterruptedException {
// Slot untuk merekam nama thread yang menjalankan body tugas.
String[] pelaksana = new String[1];
Thread t = new Thread(() -> {
// Thread.currentThread() mengembalikan thread yang sedang menjalankan
// baris ini, bisa thread main ataupun thread baru, tergantung siapa
// pemanggil run().
pelaksana[0] = Thread.currentThread().getName();
});
t.setName("pekerja");
// Eksperimen 1: memanggil run() secara langsung.
// Tidak ada thread baru; body dieksekusi sinkron oleh thread main.
t.run();
System.out.println("run() dieksekusi oleh : " + pelaksana[0]);
// Eksperimen 2: memanggil start().
// JVM membuat thread baru bernama "pekerja" dan menjalankan run() di sana.
t.start();
t.join();
System.out.println("start() dieksekusi oleh : " + pelaksana[0]);
}
}