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 #

AspekProsesThread
Ruang alamatTerpisah, dijamin oleh sistem operasiBerbagi ruang alamat proses pemiliknya
Heap (objek)Heap sendiri untuk setiap prosesSatu heap yang sama untuk semua thread
Stack (variabel lokal)Berada dalam ruang alamat prosesStack sendiri untuk setiap thread
Komunikasi dataButuh mekanisme antar-proses: pipe, socket, berkasCukup baca/tulis objek bersama
Biaya pembuatan dan context switchRelatif mahalRelatif ringan karena ruang alamat tidak berganti
Dampak kegagalanUmumnya terisolasi pada proses tersebutSatu 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:

java BerbagiHeap.java
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:

text
pekerja-1 menjumlahkan 1-100: 5050
pekerja-2 menjumlahkan 101-200: 15050
pekerja-3 menjumlahkan 201-300: 25050

Mari kita telaah baris-baris kode di atas:

  1. Baris 5 (int[] hasil = new int[3];): membuat satu objek array di heap. Thread main dan ketiga thread pekerja memegang referensi ke objek yang sama.
  2. Baris 10 (pekerja[i] = new Thread(() -> {...})): membuat thread baru dari lambda Runnable. Lambda menangkap hasil dan indeks sehingga body thread dapat menjangkau array di heap.
  3. Baris 11 (int jumlah = 0;): variabel lokal yang hidup di stack masing-masing thread. Thread pekerja-2 tidak akan pernah melihat variabel jumlah milik pekerja-1.
  4. 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.
  5. Baris 20 (t.join()): thread main menunggu setiap pekerja selesai. Setelah semua join kembali, nilai di hasil sudah terisi penuh.
  6. Baris 24-27 (System.out.println(...)): pencetakan dilakukan oleh main setelah 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 #

Berkas Sumber

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

BerbagiHeap.java

Unduh berkas
java
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]);
        }
    }
}