Kata kunci synchronized pada Chapter sebelumnya terasa seperti tombol serbaguna: kita menempelkannya, dan JVM yang mengurus sisanya. Mekanismenya sesungguhnya sederhana — setiap objek Java dilengkapi satu intrinsic lock (juga disebut monitor lock) yang sudah melekat sejak objek dibuat, dan synchronized hanyalah cara meminjamnya. Bab ini membuka kap mesinnya: bagaimana JVM mengaitkan satu lock per objek, mengapa thread yang sudah memegang lock boleh memegangnya lagi (sifat reentrant), dan bagaimana memilih objek lock sehingga wilayah terlindung tidak lebih luas atau lebih sempit dari yang kita butuhkan.
Setiap Objek Membawa Satu Lock #
Spesifikasi JVM mewajibkan setiap objek memiliki monitor internal yang menyimpan identitas thread pemegang lock beserta jumlahnya. Objek Object paling polos sekalipun — hasil new Object() — membawa fasilitas tersebut. Ketika eksekusi memasuki synchronized (objek), thread harus menjadi pemegang lock objek itu; ketika blok berakhir, kepemilikan dilepas dan thread berikutnya di antrean boleh maju.
Tiga bentuk synchronized yang sudah kita kenal hanyalah tiga pintu menuju mekanisme yang sama:
synchronized (objek) { ... }— meminjam lock objek yang kita tunjuk secara eksplisit.- Method
synchronizedinstance — meminjam lock objek pemanggil (this). - Method
static synchronized— meminjam lock objekClassdari class yang bersangkutan.
Konsekuensi pentingnya satu kalimat: penyimpanan lock pada objek, bukan pada kode. Potongan kode yang sama bisa diamankan beberapa lock berbeda bila dieksekusi pada objek berbeda, dan potongan kode berbeda bisa saling menunggu bila keduanya meminjam lock objek yang sama. Membaca program konkuren dengan benar berarti melacak objek mana yang dikunci, bukan sekadar melihat kata synchronized.
Thread Boleh Memegang Locknya Berulang #
Intrinsic lock bersifat reentrant: thread yang sudah menjadi pemilik boleh memperoleh lock yang sama berulang kali. JVM menghitung jumlah kepemilikan itu (misalnya masuk dua blok bersarang = kepemilikan dua), dan lock benar-benar dilepas hanya ketika seluruh blok bersarang telah keluar. Sifat ini bukan kemewahan — tanpa sifat ini, method synchronized yang memanggil method synchronized lain pada objek yang sama akan menunggu dirinya sendiri selamanya.
Program berikut membuktikan dua hal sekaligus: pemanggilan berantai yang aman berkat reentrancy, dan method synchronized kedua yang benar-benar menunggu thread pertama selesai:
import java.util.concurrent.ConcurrentLinkedQueue;
public class Reentransi {
// Antrean kejadian: urutan sebenarnya dijaga happens-before tiapa offer,
// lalu seluruh isi dicetak setelah kedua thread selesai.
private static final ConcurrentLinkedQueue<String> kejadian = new ConcurrentLinkedQueue<>();
private static class Layanan {
public synchronized void prosesUtama(String nama) {
kejadian.add(nama + " masuk prosesUtama");
try {
Thread.sleep(100); // pegang monitor selama 100 ms
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
prosesTambahan(nama); // method synchronized lain pada objek yang sama
kejadian.add(nama + " keluar prosesUtama");
}
public synchronized void prosesTambahan(String nama) {
kejadian.add(nama + " masuk prosesTambahan");
}
}
public static void main(String[] args) throws InterruptedException {
Layanan layanan = new Layanan();
Thread a = new Thread(() -> layanan.prosesUtama("pekerja-A"), "pekerja-A");
Thread b = new Thread(() -> layanan.prosesTambahan("pekerja-B"), "pekerja-B");
a.start();
b.start();
a.join();
b.join();
for (String baris : kejadian) {
System.out.println(baris);
}
}
}Output yang Dihasilkan:
pekerja-A masuk prosesUtama
pekerja-A masuk prosesTambahan
pekerja-A keluar prosesUtama
pekerja-B masuk prosesTambahanMari kita telaah kronologi yang tercetak:
pekerja-A masuk prosesUtama: pekerja-A menjadi pemilik lock objeklayanan(hitungan kepemilikan = 1), lalu tidur 100 milidetik — lock tetap terpegang selama tidur.- Absennya pekerja-B di awal: pekerja-B memanggil
prosesTambahansejak awal, tetapi berstateBLOCKEDmenunggu lock; tidak ada satu pun baris pekerja-B pada paruh pertama keluaran. pekerja-A masuk prosesTambahan: pemanggilan methodsynchronizedkedua pada objek yang sama. Karena pemilik lock tetap pekerja-A, masuk kembali diizinkan tanpa menunggu (hitungan kepemilikan = 2). Bila lock tidak reentrant, baris ini tidak akan pernah terjadi — pekerja-A menunggu dirinya sendiri.pekerja-A keluar prosesUtama: methodprosesUtamaselesai, hitungan kepemilikan turun ke nol, lock lepas.pekerja-B masuk prosesTambahan: baru setelah lock benar-benar lepas, pekerja-B yang sudah lama menunggu berhasil masuk — bukti eksklusi tetap bekerja di antara thread yang berbeda.
Satu Objek Satu Monitor #
Sisi lain dari “lock menempel pada objek” adalah: dua objek berbeda berarti dua lock berbeda. Program berikut menggunakan dua objek Object polos sebagai dua pintu terpisah, lalu membuktikan bahwa Siti dapat melewati pintu kedua tanpa menunggu Budi yang masih berdiri di pintu pertama:
public class MonitorPerObjek {
// Flag volatile agar tulisan Budi langsung terlihat oleh Siti
// (penjelasan lengkap ada pada Chapter volatile).
private static volatile boolean budiDiDalam = false;
public static void main(String[] args) throws InterruptedException {
Object pintu1 = new Object(); // setiap objek punya intrinsic lock sendiri
Object pintu2 = new Object();
boolean[] bukti = new boolean[1];
Thread budi = new Thread(() -> {
synchronized (pintu1) {
budiDiDalam = true;
try {
Thread.sleep(300); // memegang lock pintu1 selama 300 ms
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
budiDiDalam = false;
}
}, "Budi");
Thread siti = new Thread(() -> {
try {
Thread.sleep(100); // pastikan Budi sudah berada di dalam pintu1
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
synchronized (pintu2) { // lock pintu2 berbeda: masuk tanpa menunggu
bukti[0] = budiDiDalam; // true berarti Budi masih di dalam pintu1
}
}, "Siti");
budi.start();
siti.start();
budi.join();
siti.join();
System.out.println("Siti masuk pintu-2 saat Budi masih di pintu-1: " + bukti[0]);
}
}Output yang Dihasilkan:
Siti masuk pintu-2 saat Budi masih di pintu-1: trueTiga detail desain di balik bukti true tersebut:
Object pintu1 = new Object();danObject pintu2 = new Object();: dua objek polos tanpa data apa pun — nilainya justru pada intrinsic lock yang masing-masing dibawa. Objek semacam ini memang pola umum untuk lock yang tidak ingin dicampur dengan logika bisnis.Thread.sleep(100)pada Siti: memberi waktu Budi memasuki wilayahsynchronized (pintu1). Pada saat Siti memasukisynchronized (pintu2)di sekitar milidetik ke-100, Budi masih terpegang di pintu1 hingga milidetik ke-300.bukti[0] = budiDiDalam;: dibaca di dalam wilayahsynchronized (pintu2)— pemegangan lock pintu2 terjadi bersamaan dengan Budi masih berada di dalam pintu1. Nilaitrueadalah bukti tumpang-tindih tersebut; dua wilayah ini tidak saling menunggu karena monitornya berbeda objek.
Pembacaan flag budiDiDalam antar thread membutuhkan jaminan visibilitas, sehingga flag ditandai volatile — peran yang akan dibedah khusus pada Chapter berikutnya. Untuk contoh ini cukup ingat: tulisan Budi ke flag langsung terlihat oleh Siti.
wait dan notify Milik Intrinsic Lock #
Monitor objek tidak hanya menyimpan lock, tetapi juga satu wait set — ruang tunggu bagi thread pemegang lock yang menyerah sementara. Method wait(), notify(), dan notifyAll() dari class Object mengoperasikan ruang tunggu tersebut, dan ketiganya hanya boleh dipanggil oleh thread yang sedang memegang lock objek itu; melanggarnya melempar IllegalMonitorStateException. Skema koordinasi wait/notify layak dibahas dalam babnya sendiri dan menunggu di paruh kedua Section ini — cukup dicatat di sini bahwa fasilitas tersebut bukan fitur tambahan, melainkan bagian tak terpisahkan dari monitor yang sama.
Intrinsic lock menyatukan dua jaminan sekaligus — eksklusi dan visibilitas. Pertanyaan menarik berikutnya: bila kita hanya butuh jaminan kedua, bahwa tulisan sebuah thread terlihat thread lain tanpa eksklusi, apakah ada alat yang lebih ringan? Jawabannya adalah kata kunci volatile.
Referensi #
- Oracle: The Java Tutorials — Intrinsic Locks and Synchronization
- Oracle: Class Object (Java API Specification)
- Oracle: Java Language Specification — Synchronization (Chapter 17)
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
MonitorPerObjek.java
Unduh berkaspublic class MonitorPerObjek {
// Flag volatile agar tulisan Budi langsung terlihat oleh Siti
// (penjelasan lengkap ada pada Chapter volatile).
private static volatile boolean budiDiDalam = false;
public static void main(String[] args) throws InterruptedException {
Object pintu1 = new Object(); // setiap objek punya intrinsic lock sendiri
Object pintu2 = new Object();
boolean[] bukti = new boolean[1];
Thread budi = new Thread(() -> {
synchronized (pintu1) {
budiDiDalam = true;
try {
Thread.sleep(300); // memegang lock pintu1 selama 300 ms
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
budiDiDalam = false;
}
}, "Budi");
Thread siti = new Thread(() -> {
try {
Thread.sleep(100); // pastikan Budi sudah berada di dalam pintu1
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
synchronized (pintu2) { // lock pintu2 berbeda: masuk tanpa menunggu
bukti[0] = budiDiDalam; // true berarti Budi masih di dalam pintu1
}
}, "Siti");
budi.start();
siti.start();
budi.join();
siti.join();
System.out.println("Siti masuk pintu-2 saat Budi masih di pintu-1: " + bukti[0]);
}
}
Reentransi.java
Unduh berkasimport java.util.concurrent.ConcurrentLinkedQueue;
public class Reentransi {
// Antrean kejadian: urutan sebenarnya dijaga happens-before tiapa offer,
// lalu seluruh isi dicetak setelah kedua thread selesai.
private static final ConcurrentLinkedQueue<String> kejadian = new ConcurrentLinkedQueue<>();
private static class Layanan {
public synchronized void prosesUtama(String nama) {
kejadian.add(nama + " masuk prosesUtama");
try {
Thread.sleep(100); // pegang monitor selama 100 ms
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
prosesTambahan(nama); // method synchronized lain pada objek yang sama
kejadian.add(nama + " keluar prosesUtama");
}
public synchronized void prosesTambahan(String nama) {
kejadian.add(nama + " masuk prosesTambahan");
}
}
public static void main(String[] args) throws InterruptedException {
Layanan layanan = new Layanan();
Thread a = new Thread(() -> layanan.prosesUtama("pekerja-A"), "pekerja-A");
Thread b = new Thread(() -> layanan.prosesTambahan("pekerja-B"), "pekerja-B");
a.start();
b.start();
a.join();
b.join();
for (String baris : kejadian) {
System.out.println(baris);
}
}
}