Sebelum menulis kode multithreading satu baris pun, kita perlu memahami dua istilah fondasinya: proses dan thread. Aplikasi modern jarang mengerjakan satu hal dalam satu waktu — server web menerima ribuan permintaan, aplikasi desktop merender tampilan sambil mengunduh berkas, dan game menghitung fisika sambil memutar audio. Java menyediakan API thread yang kaya, tetapi API tersebut hanya masuk akal bila kita tahu unit eksekusi mana yang berbagi memori dan mana yang tidak. Pada Chapter ini kita bedah konsep proses dan thread, membandingkan keduanya dari sisi memori hingga biaya perpindahan, lalu membuktikan lewat kode bahwa seluruh thread dalam satu proses memakai heap yang sama.
Proses adalah Program yang Sedang Berjalan #
Program hanyalah berkas kode di penyimpanan — pasif dan tidak melakukan apa-apa. Ketika kita menjalankan java Aplikasi, sistem operasi menyalin program tersebut ke memori, menyiapkan ruang alamat tersendiri, lalu mengeksekusinya; bentuk hidup inilah yang disebut proses. Setiap proses memiliki ruang alamat sendiri berisi kode, heap untuk objek, dan sumber daya seperti file descriptor. Sistem operasi menjamin proses lain tidak dapat menjangkau memori proses tersebut, sehingga kegagalan satu proses umumnya tidak melumpuhkan proses lain.
Saat kita menjalankan program Java, satu proses baru lahir dan JVM mulai bekerja di dalamnya: memuat bytecode, menjalankan garbage collector, dan mengeksekusi method main. Dengan kata lain, setiap program Java berjalan sebagai satu proses sistem operasi — dan di dalam proses itulah seluruh thread Java kita hidup.
Thread adalah Unit Eksekusi di Dalam Proses #
Thread adalah unit eksekusi terkecil yang dijadwalkan sistem operasi: satu alur perintah dengan stack sendiri untuk variabel lokal dan program counter yang menandai posisi eksekusi. Beberapa thread hidup berdampingan di dalam satu proses dan berbagi sumber daya proses tersebut — terutama heap tempat semua objek berada, termasuk variabel static. JVM memulai program kita di thread bernama main, dan JVM juga menjalankan thread internal seperti garbage collector.
Akibat pembagian tersebut bersifat dua arah. Keuntungannya, bertukar data antar thread semudah membaca dan menulis field pada objek yang sama, tanpa mekanisme komunikasi khusus. Bahayanya, dua thread dapat menulis ke memori yang sama pada waktu yang hampir bersamaan — situasi inilah yang menjadi akar race condition pada Chapter-chapter berikutnya.
Perbandingan Proses dan Thread #
| Aspek | Proses | Thread |
|---|---|---|
| Ruang alamat | Terpisah, dijamin oleh sistem operasi | Berbagi ruang alamat proses pemiliknya |
| Heap (objek) | Heap sendiri untuk setiap proses | Satu heap yang sama untuk semua thread |
| Stack (variabel lokal) | Berada dalam ruang alamat proses | Stack sendiri untuk setiap thread |
| Komunikasi data | Butuh mekanisme antar-proses: pipe, socket, berkas | Cukup baca/tulis objek bersama |
| Biaya pembuatan dan context switch | Relatif mahal | Relatif ringan karena ruang alamat tidak berganti |
| Dampak kegagalan | Umumnya terisolasi pada proses tersebut | Satu thread yang gagal dapat menghentikan seluruh proses |
Konteks Switch dan Penjadwalan #
Satu core CPU hanya menjalankan satu thread pada satu saat. Rasa “bersamaan” yang kita rasakan lahir dari penjadwal: sistem operasi memutar-mutar thread di atas core secara bergantian dalam potongan waktu (timeslice) yang sangat pendek. Ketika giliran sebuah thread berganti, sistem operasi menyimpan register dan posisi eksekusinya lalu memuat keadaan thread berikutnya — peristiwa ini disebut context switch.
Context switch antar thread dalam satu proses lebih murah daripada antar proses. Alasannya, kedua thread memakai pemetaan memori yang sama sehingga sistem operasi tidak perlu menukar tabel halaman. Inilah sebabnya thread sering dipilih untuk tugas yang padat koordinasi data, sedangkan proses dipilih ketika isolasi lebih penting.
Membuktikan Heap yang Dibagi #
Teori di atas bisa kita buktikan langsung. Program berikut membuat satu objek array hasil di heap, lalu tiga thread mengisi slot masing-masing pada array yang sama. Setiap thread menjumlahkan rentang bilangan yang berbeda; variabel jumlah tetap lokal di stack thread sehingga tidak saling terganggu:
public class BerbagiHeap {
public static void main(String[] args) throws InterruptedException {
// Satu objek array di heap; semua thread di proses yang sama melihatnya.
int[] hasil = new int[3];
String[] rentang = {"1-100", "101-200", "201-300"};
Thread[] pekerja = new Thread[3];
for (int i = 0; i < pekerja.length; i++) {
int indeks = i; // variabel efektif final agar bisa dipakai lambda
pekerja[i] = new Thread(() -> {
int jumlah = 0; // variabel lokal: hidup di stack thread ini
int awal = indeks * 100 + 1;
int akhir = (indeks + 1) * 100;
for (int n = awal; n <= akhir; n++) {
jumlah += n;
}
hasil[indeks] = jumlah; // menulis ke objek bersama di heap
});
pekerja[i].setName("pekerja-" + (i + 1));
pekerja[i].start();
}
for (Thread t : pekerja) {
t.join();
}
// Dicetak setelah semua join sehingga urutan keluaran pasti tetap.
for (int i = 0; i < hasil.length; i++) {
System.out.println(pekerja[i].getName() + " menjumlahkan "
+ rentang[i] + ": " + hasil[i]);
}
}
}Output yang Dihasilkan:
pekerja-1 menjumlahkan 1-100: 5050
pekerja-2 menjumlahkan 101-200: 15050
pekerja-3 menjumlahkan 201-300: 25050Mari kita telaah baris-baris kode di atas:
- Baris 5 (
int[] hasil = new int[3];): membuat satu objek array di heap. Threadmaindan ketiga thread pekerja memegang referensi ke objek yang sama. - Baris 10 (
pekerja[i] = new Thread(() -> {...})): membuat thread baru dari lambdaRunnable. Lambda menangkaphasildanindekssehingga body thread dapat menjangkau array di heap. - Baris 11 (
int jumlah = 0;): variabel lokal yang hidup di stack masing-masing thread. Thread pekerja-2 tidak akan pernah melihat variabeljumlahmilik pekerja-1. - Baris 16 (
hasil[indeks] = jumlah;): satu-satunya jalur komunikasi — menulis ke slot tetap pada objek bersama. Setiap thread menulis ke indeks berbeda sehingga tidak saling menimpa. - Baris 20 (
t.join()): threadmainmenunggu setiap pekerja selesai. Setelah semuajoinkembali, nilai dihasilsudah terisi penuh. - Baris 24-27 (
System.out.println(...)): pencetakan dilakukan olehmainsetelah semua selesai, bukan dari dalam thread, sehingga urutan keluaran selalu tetap.
Perhatikan pola penting pada contoh di atas: setiap thread menulis ke slot indeks yang tetap, lalu main mencetak hasil setelah semua join. Pola “kumpulkan lalu cetak setelah join” ini akan kita pakai berulang kali di seluruh Section karena menjadikan keluaran program konkuren tetap terprediksi.
Kapan Proses Lebih Tepat #
Isolasi antar proses bukan kelemahan, melainkan fitur. Browser menjalankan setiap tab sebagai proses terpisah agar satu halaman yang gagal tidak melumpuhkan seluruh aplikasi; arsitektur microservice memisahkan komponen sistem ke proses (bahkan mesin) berbeda demi batas kegagalan yang tegas. Pilih thread ketika unit kerja perlu berbagi data secara rapat dan ringan; pilih proses ketika kegagalan satu unit tidak boleh menjalar ke unit lain.
Dengan bekal konsep proses dan thread, kita siap membuat thread pertama secara nyata: membandingkan jalur extends Thread, jalur Runnable, serta perbedaan mendasar antara start() dan run() pada Chapter berikutnya.
Referensi #
- Oracle: Class Thread (Java API Specification)
- Oracle: Java Concurrency (dev.java)
- Oracle: The Java Tutorials — Concurrency
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
BerbagiHeap.java
Unduh berkaspublic class BerbagiHeap {
public static void main(String[] args) throws InterruptedException {
// Satu objek array di heap; semua thread di proses yang sama melihatnya.
int[] hasil = new int[3];
String[] rentang = {"1-100", "101-200", "201-300"};
Thread[] pekerja = new Thread[3];
for (int i = 0; i < pekerja.length; i++) {
int indeks = i; // variabel efektif final agar bisa dipakai lambda
pekerja[i] = new Thread(() -> {
int jumlah = 0; // variabel lokal: hidup di stack thread ini
int awal = indeks * 100 + 1;
int akhir = (indeks + 1) * 100;
for (int n = awal; n <= akhir; n++) {
jumlah += n;
}
hasil[indeks] = jumlah; // menulis ke objek bersama di heap
});
pekerja[i].setName("pekerja-" + (i + 1));
pekerja[i].start();
}
for (Thread t : pekerja) {
t.join();
}
// Dicetak setelah semua join sehingga urutan keluaran pasti tetap.
for (int i = 0; i < hasil.length; i++) {
System.out.println(pekerja[i].getName() + " menjumlahkan "
+ rentang[i] + ": " + hasil[i]);
}
}
}