Sepanjang Section ini kita telah membedah alat koordinasi satu per satu. Sekarang saatnya menaikkan ketinggian pandang: apa yang sebenarnya membuat sebuah class thread-safe, dan mengapa class tanpa perlindungan bisa tampak bekerja baik-baik saja lalu tumbang di hari tersibuk? Thread safety bukan fitur yang ditambahkan belakangan melainkan properti desain: data yang tidak dibagi tidak perlu dilindungi, dan data yang dibagi harus dilindungi dengan alat yang tepat. Pada Chapter ini kita akan memetakan tiga sumber bahaya — visibility, atomicity, dan publication — lalu menyusun resep praktis untuk masing-masing.
Tiga Sumber Bahaya #
Visibility berkaitan dengan apakah perubahan yang dilakukan satu thread terlihat thread lain. Model memori Java (Java Memory Model) mengizinkan setiap thread bekerja pada salinan cache nilai field, dan compiler serta prosesor dibebaskan menyusun ulang instruksi selama program tunggal tampak berurutan. Akibatnya, thread pembaca bisa terus melihat nilai lama padahal penulis sudah menggantinya. volatile, synchronized, dan join() membangun happens-before — jaminan bahwa tulisan sebelum titik tersebut terlihat oleh pembaca setelahnya.
Atomicity berkaitan dengan operasi yang tampak satu langkah padahal beberapa. Ekspresi total++ membaca, menaikkan, lalu menulis kembali; dua thread yang berjalan bersamaan dapat membaca nilai yang sama dan kedua kenaikan kehilangan satu hasil. Chapter Race Conditions telah memperlihatkan lost update ini secara konkret. Operasi check-then-act seperti if (map.isEmpty()) map.put(...) menderita masalah yang sama.
Publication berkaitan dengan objek yang terlihat “setengah jadi”. Tanpa sinkronisasi yang benar, thread pembaca dapat melihat field default (null, 0) sebelum constructor selesai menulis nilai akhirnya — objek berstatus tidak konsisten bocor keluar.
Resep Aman: Empat Jalur #
Kabar baiknya: seluruh bahaya di atas dibungkus oleh segelitir strategi yang sudah kita kenal. Tabel berikut merangkum empat jalur utama beserta kapan memakainya:
| Strategi | Prinsip | Alat di Java | Cocok untuk |
|---|---|---|---|
| Immutability | Objek tak berubah tak perlu dilindungi | record, final, String | Data nilai, DTO, konfigurasi |
| Confinement | Data hanya disentuh satu thread | variabel lokal, ThreadLocal | Perhitungan per-request |
| Alat atomik | Satu variabel dibagi | AtomicLong, ConcurrentHashMap | counter, cache, statistik |
| Lock | Blok kode tidak boleh bersamaan | synchronized, ReentrantLock | invarian lintas variabel |
Urutan tabel tersebut juga urutan preferensi: mulai dari yang paling murah diurai. Objek immutable dibagi bebas ke ribuan thread tanpa risiko apa pun; kebutuhan akan lock seharusnya muncul hanya ketika invarian mengikat beberapa variabel sekaligus yang memang harus berubah bersama.
Immutable + Atomic di Praktik #
Program berikut menggabungkan dua jalur pertama dan ketiga: data tagihan dibuat immutable dengan record, sedangkan akumulator yang dibagi dijaga AtomicLong:
import java.util.concurrent.atomic.AtomicLong;
public class ObjekAman {
// Immutable: seluruh field final, disalin saat konstruksi, tak bisa berubah
record Tagihan(String pelanggan, long jumlahTagihan) {
}
// State dibagi dijaga class concurrency -- bukan field telanjang
static class Pembukuan {
private final AtomicLong total = new AtomicLong();
void catat(Tagihan t) {
total.addAndGet(t.jumlahTagihan());
}
long total() {
return total.get();
}
}
public static void main(String[] args) throws InterruptedException {
Tagihan[] tagihan = {
new Tagihan("Budi", 150_000),
new Tagihan("Siti", 250_000),
new Tagihan("Dewi", 75_000),
new Tagihan("Ani", 120_000),
new Tagihan("Rahmat", 330_000),
};
Pembukuan pembukuan = new Pembukuan();
Thread[] pekerja = new Thread[tagihan.length];
for (int i = 0; i < tagihan.length; i++) {
final Tagihan t = tagihan[i];
pekerja[i] = new Thread(() -> pembukuan.catat(t));
pekerja[i].start();
}
for (Thread p : pekerja) {
p.join();
}
System.out.println("total tercatat : " + pembukuan.total());
System.out.println("total harapan : " + 925_000);
}
}Output yang Dihasilkan:
total tercatat : 925000
total harapan : 925000Mari kita bedah pilar keamanan program tersebut:
record Tagihan(...): seluruh komponen bersifat final dan terisi penuh sebelum constructor selesai — final field freeze guarantee memastikan setiap thread yang memperoleh referensi ini melihat isi utuh, tanpa perlu lock apa pun.PembukuanmemakaiAtomicLong:addAndGetmenjalankan operasi baca-naik-tulis sebagai satu kesatuan melalui compare-and-swap prosesor, sehingga lima thread dapat mencatat bersamaan tanpa kehilangan satu rupiah pun.join()sebelum pencetakan: selain menunggu pekerjaan selesai,joinjuga membentuk happens-before, sehingga pembacaantotal()dimainpasti melihat seluruh tulisan para pekerja.
Jebakan Publikasi yang Sering Terlewat #
Dua pola berikut tampak wajar namun menyimpan risiko publication. Pertama, objek mutable dipublikasikan lewat field static non-final sebelum constructor selesai — thread lain dapat melihat objek setengah jadi. Kedua, lazy initialization tanpa sinkronisasi if (instance == null) instance = buat(); — dua thread dapat sama-sama melihat null dan membuat dua objek, atau yang lebih buruk, satu thread melihat referensi non-null yang field-nya belum terisi. Penawar keduanya sederhana: deklarasikan sebagai static final (pembuatan di class initialization dijamin aman oleh JLS), atau rapat pemeriksaan dalam synchronized, atau serahkan pada class java.util.concurrent.atomic dan framework yang sudah menguji polanya.
Kaidah penutup yang layak ditempel di monitor: tidak berbagi berarti aman; berbagi tanpa berubah cukup dengan immutable; berbagi dan berubah menuntut alat concurrency yang tepat. Dengan tiga kalimat tersebut, mayoritas keputusan thread safety dapat diambil tanpa membuka bab spesifikasi. Chapter penutup section ini merangkai seluruh alat menjadi satu daftar praktik terbaik yang bisa dipegang saat merancang kode paralel.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
ObjekAman.java
Unduh berkasimport java.util.concurrent.atomic.AtomicLong;
public class ObjekAman {
// Immutable: seluruh field final, disalin saat konstruksi, tak bisa berubah
record Tagihan(String pelanggan, long jumlahTagihan) {
}
// State dibagi dijaga class concurrency -- bukan field telanjang
static class Pembukuan {
private final AtomicLong total = new AtomicLong();
void catat(Tagihan t) {
total.addAndGet(t.jumlahTagihan());
}
long total() {
return total.get();
}
}
public static void main(String[] args) throws InterruptedException {
Tagihan[] tagihan = {
new Tagihan("Budi", 150_000),
new Tagihan("Siti", 250_000),
new Tagihan("Dewi", 75_000),
new Tagihan("Ani", 120_000),
new Tagihan("Rahmat", 330_000),
};
Pembukuan pembukuan = new Pembukuan();
Thread[] pekerja = new Thread[tagihan.length];
for (int i = 0; i < tagihan.length; i++) {
final Tagihan t = tagihan[i];
pekerja[i] = new Thread(() -> pembukuan.catat(t));
pekerja[i].start();
}
for (Thread p : pekerja) {
p.join();
}
System.out.println("total tercatat : " + pembukuan.total());
System.out.println("total harapan : " + 925_000);
}
}