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:

text
counter instance : 200000
counter statis   : 200000

Mari kita telaah baris-baris kode di atas:

  1. Baris 7 (synchronized void naik()): seluruh body naik() dilindungi monitor milik objek instance. Dua thread yang memanggil counter.naik() pada objek counter yang sama akan bergantian; memanggil pada objek Counter berbeda tidak saling menunggu karena monitornya berbeda.
  2. Baris 12 (static synchronized void naikStatis()): lock-nya adalah objek Class untuk Counter — bisa ditulis eksplisit sebagai synchronized (Counter.class). Karena hanya ada satu objek Class tersebut, seluruh instance di seluruh program berbagi lock yang sama.
  3. 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 nilai dan statis tidak pernah disentuh dua kelompok sekaligus.
  4. 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:

text
saldo akhir: 200000

Tiga 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:

java Pola lock yang berisiko
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 menunggu
java Pola lock yang disarankan
class 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 #

AspekMethod synchronizedBlok synchronized
Wilayah terlindungiSeluruh body methodPotongan kode yang kita pilih
Objek lockthis (instance) atau objek Class (static)Objek apa pun yang kita tunjuk
GranularitasKasar — semua ikut terkunciHalus — hanya bagian kritis
Kontrol dari luarLock terikat identitas objek bisnisLock bisa disembunyikan sebagai field privat
KecocokanMethod pendek yang seluruhnya kritisMethod 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 #

Berkas Sumber

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

BlokSynchronized.java

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

MetodeSynchronized.java

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