Pada Chapter Race Conditions, satu kata kunci kecil menyelamatkan counter dari hasil undian: synchronized. Kata kunci ini adalah mekanisme koordinasi tertua sekaligus paling banyak dipakai di Java, dan memahaminya secara utuh berarti memahami tiga hal: bentuk penulisannya (method atau blok), objek lock mana yang sebenarnya dipakai, dan wilayah kode sekecil apa yang perlu dikunci. Bab ini membahas ketiganya, termasuk perbedaan lock level instance dan lock level class, serta praktik terbaik memilih objek lock yang tidak bisa dijamah kode luar.
Cara Kerja synchronized #
Menggunakan synchronized berarti meminjam monitor milik sebuah objek: struktur data internal yang setiap objek Java bawaan sejak lahir. Thread yang ingin memasuki wilayah synchronized harus memegang monitor objek tersebut; bila monitor sedang dipegang thread lain, thread peminjam menunggu dengan state BLOCKED — persis state yang kita lihat pada Chapter Thread Lifecycle. Monitor hanya bisa dipegang satu thread pada satu waktu, sehingga seluruh kode yang dilindungi monitor yang sama berjalan satu per satu.
Dua efek penting mengikuti aturan tersebut. Pertama, mutual exclusion: operasi read-modify-write di dalam wilayah terlindung tidak bisa saling menyisip, sehingga race condition pada data yang dijaga hilang. Kedua, visibilitas memori: thread yang berhasil memasuki monitor dijamin melihat seluruh tulisan yang dilakukan thread pemegang monitor sebelumnya — detail ini kita bahas serius pada Chapter volatile.
Method synchronized #
Penanda synchronized pada method membuat seluruh body method menjadi wilayah terlindung. Untuk method instance, lock yang dipakai adalah objek pemanggil method (this); untuk method static, lock-nya adalah objek Class<?> milik class tersebut — satu-satunya untuk seluruh instance. Program berikut menguji keduanya sekaligus:
public class MetodeSynchronized {
private static class Counter {
private long nilai = 0;
private static long statis = 0;
// Lock: objek instance Counter yang menjadi pemanggil method.
synchronized void naik() {
nilai++;
}
// Lock: objek Class<?> milik Counter (satu untuk seluruh instance).
static synchronized void naikStatis() {
statis++;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable tugasInstansi = () -> {
for (int i = 0; i < 100_000; i++) {
counter.naik();
}
};
Runnable tugasStatis = () -> {
for (int i = 0; i < 100_000; i++) {
Counter.naikStatis();
}
};
Thread a = new Thread(tugasInstansi, "teller-A");
Thread b = new Thread(tugasInstansi, "teller-B");
Thread c = new Thread(tugasStatis, "teller-C");
Thread d = new Thread(tugasStatis, "teller-D");
a.start();
b.start();
c.start();
d.start();
a.join();
b.join();
c.join();
d.join();
// Pembacaan setelah join() aman: join membangun happens-before.
System.out.println("counter instance : " + counter.nilai);
System.out.println("counter statis : " + Counter.statis);
}
}Output yang Dihasilkan:
counter instance : 200000
counter statis : 200000Mari kita telaah baris-baris kode di atas:
- Baris 7 (
synchronized void naik()): seluruh bodynaik()dilindungi monitor milik objek instance. Dua thread yang memanggilcounter.naik()pada objekcounteryang sama akan bergantian; memanggil pada objekCounterberbeda tidak saling menunggu karena monitornya berbeda. - Baris 12 (
static synchronized void naikStatis()): lock-nya adalah objekClassuntukCounter— bisa ditulis eksplisit sebagaisynchronized (Counter.class). Karena hanya ada satu objekClasstersebut, seluruh instance di seluruh program berbagi lock yang sama. - Baris 27-34 (empat thread): thread A dan B berebut monitor instance, thread C dan D berebut monitor class. Dua kelompok tersebut berjalan pada dua lock yang berbeda dan saling bebas — memori
nilaidanstatistidak pernah disentuh dua kelompok sekaligus. - Baris 42-43: kedua total tercetak
200000— hasil deterministik, bukan kebetulan penjadwal.
Blok synchronized untuk Wilayah yang Lebih Kecil #
Method synchronized mengunci seluruh body — boros bila method berisi validasi panjang atau komputasi yang tidak menyentuh data bersama. Bentuk blok memungkinkan kita memilih potongan mana yang benar-benar kritis dan memilih objek lock secara eksplisit:
public class BlokSynchronized {
private static class Kas {
private long saldo = 0;
// Objek lock privat: tidak ada kode luar yang bisa mengunci objek ini.
private final Object kunci = new Object();
void setor(long jumlah) {
// Validasi berjalan di luar lock: bagian non-kritis tetap paralel.
if (jumlah <= 0) {
return;
}
synchronized (kunci) {
// Bagian kritis: baca-hitung-tulis saldo sebagai satu kesatuan.
long baca = saldo;
saldo = baca + jumlah;
}
}
long bacaSaldo() {
synchronized (kunci) {
return saldo;
}
}
}
public static void main(String[] args) throws InterruptedException {
Kas kas = new Kas();
Runnable tugas = () -> {
for (int i = 0; i < 100_000; i++) {
kas.setor(1);
}
};
Thread a = new Thread(tugas, "teller-A");
Thread b = new Thread(tugas, "teller-B");
a.start();
b.start();
a.join();
b.join();
System.out.println("saldo akhir: " + kas.bacaSaldo());
}
}Output yang Dihasilkan:
saldo akhir: 200000Tiga keputusan desain pada class Kas layak dicermati. Pertama, validasi argumen (jumlah <= 0) berada di luar blok — pekerjaan yang tidak menyentuh saldo tidak perlu ikut mengunci. Kedua, hanya empat baris baca-hitung-tulis yang dilindungi, sehingga thread lain menunggu sesingkat mungkin. Ketiga, method setor dan bacaSaldo memakai objek kunci yang sama — dua method yang menyentuh saldo harus dijaga satu monitor, sebab lock yang berbeda pada data yang sama tidak melindungi apa-apa.
Memilih Objek Lock yang Aman #
Objek apa pun bisa menjadi lock, tetapi pilihan yang ceroboh membuka pintu masalah. Bandingkan dua pola berikut:
class KasRisiko {
private long saldo = 0;
void setor(long jumlah) {
synchronized (this) { // lock-nya bisa dijamah kode luar
saldo += jumlah;
}
}
}
// Kode luar bisa mengunci objek KasRisiko dan membuat program macet total:
// KasRisiko kas = new KasRisiko();
// synchronized (kas) { Thread.sleep(Long.MAX_VALUE); } // semua teller menungguclass KasAman {
private long saldo = 0;
private final Object kunci = new Object(); // privat, final, tidak bisa diganti
void setor(long jumlah) {
synchronized (kunci) {
saldo += jumlah;
}
}
}Pola synchronized (this) membuat identitas lock sama dengan identitas objek bisnis — kode luar yang memegang referensi objek bisa menulis synchronized (kas) dan mengunci jalur seluruh method. Objek kunci yang private final tersembunyi dari dunia luar: hanya kode di dalam class yang tahu objek mana yang menjadi pengawal datanya. Modifier final juga penting — field lock yang bisa ditukar (kunci = new Object()) membuat thread-thread bisa diamankan dua objek berbeda dan saling lepas koordinasi.
Method atau Blok #
| Aspek | Method synchronized | Blok synchronized |
|---|---|---|
| Wilayah terlindungi | Seluruh body method | Potongan kode yang kita pilih |
| Objek lock | this (instance) atau objek Class (static) | Objek apa pun yang kita tunjuk |
| Granularitas | Kasar — semua ikut terkunci | Halus — hanya bagian kritis |
| Kontrol dari luar | Lock terikat identitas objek bisnis | Lock bisa disembunyikan sebagai field privat |
| Kecocokan | Method pendek yang seluruhnya kritis | Method campuran: validasi/IO di luar, mutasi di dalam |
Sepanjang Chapter ini kita memakai istilah monitor dan lock hampir bergantian, dan memang keduanya menyebut mekanisme yang sama dari sisi berbeda: monitor adalah struktur datanya, lock adalah hak memegangnya. Ada satu sifat penting yang belum kita uji — thread yang sama boleh memperoleh lock yang sedang pegang berulang kali tanpa menunggu dirinya sendiri, dan sifat reentrancy itulah yang dibedah tuntas pada Chapter Intrinsic Locks berikutnya.
Referensi #
- Oracle: The Java Tutorials — Synchronized Methods
- Oracle: The Java Tutorials — Intrinsic Locks and Synchronization
- Oracle: Class Object (Java API Specification)
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
BlokSynchronized.java
Unduh berkaspublic class BlokSynchronized {
private static class Kas {
private long saldo = 0;
// Objek lock privat: tidak ada kode luar yang bisa mengunci objek ini.
private final Object kunci = new Object();
void setor(long jumlah) {
// Validasi berjalan di luar lock: bagian non-kritis tetap paralel.
if (jumlah <= 0) {
return;
}
synchronized (kunci) {
// Bagian kritis: baca-hitung-tulis saldo sebagai satu kesatuan.
long baca = saldo;
saldo = baca + jumlah;
}
}
long bacaSaldo() {
synchronized (kunci) {
return saldo;
}
}
}
public static void main(String[] args) throws InterruptedException {
Kas kas = new Kas();
Runnable tugas = () -> {
for (int i = 0; i < 100_000; i++) {
kas.setor(1);
}
};
Thread a = new Thread(tugas, "teller-A");
Thread b = new Thread(tugas, "teller-B");
a.start();
b.start();
a.join();
b.join();
System.out.println("saldo akhir: " + kas.bacaSaldo());
}
}
MetodeSynchronized.java
Unduh berkaspublic class MetodeSynchronized {
private static class Counter {
private long nilai = 0;
private static long statis = 0;
// Lock: objek instance Counter yang menjadi pemanggil method.
synchronized void naik() {
nilai++;
}
// Lock: objek Class<?> milik Counter (satu untuk seluruh instance).
static synchronized void naikStatis() {
statis++;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable tugasInstansi = () -> {
for (int i = 0; i < 100_000; i++) {
counter.naik();
}
};
Runnable tugasStatis = () -> {
for (int i = 0; i < 100_000; i++) {
Counter.naikStatis();
}
};
Thread a = new Thread(tugasInstansi, "teller-A");
Thread b = new Thread(tugasInstansi, "teller-B");
Thread c = new Thread(tugasStatis, "teller-C");
Thread d = new Thread(tugasStatis, "teller-D");
a.start();
b.start();
c.start();
d.start();
a.join();
b.join();
c.join();
d.join();
// Pembacaan setelah join() aman: join membangun happens-before.
System.out.println("counter instance : " + counter.nilai);
System.out.println("counter statis : " + Counter.statis);
}
}