synchronized andal untuk kebanyakan kasus, tetapi bentuknya kaku: thread yang menunggu monitor tidak bisa dibatasi waktunya, tidak bisa dibangunkan lewat interrupt(), dan urutan pemberian giliran diserahkan sepenuhnya ke JVM. Ketika kebutuhan menuntut kendali lebih — misalnya “coba ambil lock selama 50 milidetik, kalau gagal lewati tugas ini” — kita membutuhkan lock eksplisit. Class ReentrantLock dari java.util.concurrent.locks menawarkan seluruh kemampuan intrinsic lock ditambah tiga fitur yang tidak dimiliki synchronized: tryLock dengan batas waktu, kepekaan terhadap interupsi, dan opsi fairness. Kekuatan ekstra itu dibayar dengan satu kewajiban disiplin: unlock() harus dipanggil di blok finally.

Bentuk Dasar dan Aturan finally #

Pola pemakaian ReentrantLock selalu tiga baris: lock() untuk masuk, try membungkus wilayah kritis, finally memanggil unlock(). Bila penulisan pola ini diabaikan dan pengecualian melompat keluar dari wilayah kritis sebelum unlock(), lock tertinggal terpegang selamanya dan seluruh thread lain berhenti menunggu:

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDasar {
    private static class Kas {
        private long saldo = 0;
        private final ReentrantLock kunci = new ReentrantLock();

        void setor(long jumlah) {
            kunci.lock(); // minta lock; menunggu bila sedang dipegang thread lain
            try {
                saldo += jumlah; // critical section
            } finally {
                kunci.unlock(); // WAJIB dilepas apa pun yang terjadi
            }
        }

        long bacaSaldo() {
            kunci.lock();
            try {
                return saldo;
            } finally {
                kunci.unlock();
            }
        }
    }

    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

Empat hal patut dicermati dari pola tersebut:

  1. Baris 8 (private final ReentrantLock kunci = ...): lock disimpan sebagai field private final — disiplin yang sama dengan objek lock pada Chapter synchronized, agar tidak ada kode luar yang bisa mengunci jalur kita.
  2. Baris 11-15 (lock() - try - finally - unlock()): unlock() berada di finally sehingga tetap terpanggil bila saldo += jumlah melempar pengecualian. Catatan penting: hanya thread pemegang lock yang boleh memanggil unlock(); pemanggil dari thread lain melempar IllegalMonitorStateException.
  3. Sifat reentrant: nama class bukan hiasan. Thread yang sudah memegang lock boleh memanggil lock() lagi — misalnya ketika setor() memanggil method lain yang mengunci lock yang sama — dan harus mengimbangi dengan unlock() sebanyak itu.
  4. Baris 29-30 (dua thread): hasil 200000 yang selalu benar menegaskan ReentrantLock memberi mutual exclusion setara intrinsic lock.

tryLock dengan Batas Waktu #

Fitur yang mustahil dilakukan synchronized: berusaha memperoleh lock dengan tenggat. tryLock() melaporkan keadaan saat itu tanpa menunggu; tryLock(waktu, satuan) menunggu selama batas tersebut lalu melaporkan gagal bila waktu habis. Program berikut mempraktikkan ketiganya dalam satu alur:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class TryLockTimeout {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock kunci = new ReentrantLock();

        // Percobaan 0: tryLock() tanpa batas waktu saat lock masih bebas.
        boolean bebas = kunci.tryLock(); // langsung melaporkan true/false tanpa menunggu
        System.out.println("tryLock() langsung : " + bebas);
        if (bebas) {
            kunci.unlock();
        }

        // Pemegang lock akan menahan lock selama 300 milidetik.
        Thread pemegang = new Thread(() -> {
            kunci.lock();
            try {
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            } finally {
                kunci.unlock();
            }
        }, "pemegang");
        pemegang.start();
        Thread.sleep(50); // pastikan pemegang sudah memegang lock

        // Percobaan 1: batas waktu 50 ms -- lock baru lepas di sekitar ms ke-300.
        boolean suksesCepat = kunci.tryLock(50, TimeUnit.MILLISECONDS);
        if (suksesCepat) {
            kunci.unlock(); // kebetulan berhasil: lepaskan kembali
        } else {
            System.out.println("tryLock(50 ms)     : gagal (lock masih dipegang)");
        }

        // Percobaan 2: batas waktu 1 detik -- menunggu sampai pemegang selesai.
        boolean suksesSabar = kunci.tryLock(1, TimeUnit.SECONDS);
        System.out.println("tryLock(1 detik)   : " + (suksesSabar ? "berhasil" : "gagal"));
        if (suksesSabar) {
            kunci.unlock();
        }

        pemegang.join();
    }
}

Output yang Dihasilkan:

text
tryLock() langsung : true
tryLock(50 ms)     : gagal (lock masih dipegang)
tryLock(1 detik)   : berhasil

Kronologi program tersebut:

  1. tryLock() langsung : true: lock masih bebas, jadi laporan datang seketika tanpa menunggu; hasilnya langsung dilepaskan kembali.
  2. tryLock(50 ms) : gagal: percobaan dimulai sekitar milidetik ke-50, saat pemegang masih menahan lock hingga milidetik ke-300; tenggat 50 milidetik habis lebih dulu, tryLock melaporkan false dan thread main bebas melanjutkan hidup — tidak menggantung menunggu tanpa batas.
  3. tryLock(1 detik) : berhasil: percobaan dimulai sekitar milidetik ke-100 dengan jatah satu detik; pemegang melepas lock pada milidetik ke-300, jadi keberhasilan tiba jauh sebelum tenggat.
  4. Pola if (sukses) unlock(): setiap tryLock yang berhasil harus diimbangi unlock() — kegagalan tidak boleh, karena lock tidak pernah terpegang.

Pola tryLock mengubah desain program: alih-alih semua thread menggantung di antrean lock yang sibuk, thread dapat memutuskan “terlalu sibuk, kerjakan yang lain dulu” — fondasi pola avoidance dan work-skipping pada sistem berbeban tinggi.

lockInterruptibly #

Kendali kedua yang tidak dimiliki synchronized: penantian lock yang bisa dibatalkan. Thread yang memanggil lockInterruptibly() akan menunggu lock seperti biasa, tetapi interrupt() dari thread lain membangunkannya dengan InterruptedException:

java Pola lockInterruptibly
kunci.lockInterruptibly();
try {
    // wilayah kritis
} finally {
    kunci.unlock();
}
// di thread lain: pekerja.interrupt(); -- penantian dibatalkan, bukan menunggu abadi

Tanpa fitur ini, thread yang menunggu synchronized hanya bisa menunggu sampai lock tersedia — interrupt() tidak berpengaruh pada antrean monitor. Dengan lockInterruptibly, aplikasi dapat membatalkan pekerjaan yang terjebak antrean lock lama, misalnya saat proses shutdown atau pembatalan permintaan pengguna.

Fairness #

Kendali ketiga menyangkut urutan giliran. Constructor new ReentrantLock(true) mengaktifkan mode fair: giliran diprioritaskan untuk thread yang paling lama menunggu, mendekati antrean first-in-first-out. Mode bawaan (unfair) membolehkan thread yang baru datang merebut lock di depan yang sudah lama menunggu — lebih cepat secara keseluruhan karena memanfaatkan cache yang masih hangat, tetapi bisa membuat thread tertentu menunggu lama sekali dalam beban tinggi.

java Dua mode constructor
ReentrantLock cepat = new ReentrantLock();     // unfair (bawaan): throughput terbaik
ReentrantLock adil  = new ReentrantLock(true); // fair: giliran menunggu terlama

synchronized atau ReentrantLock #

AspeksynchronizedReentrantLock
BentukKata kunci bahasaObjek dari pustaka java.util.concurrent.locks
Pelepasan lockOtomatis saat blok/method berakhirManual — unlock() di finally
Batas waktuTidak adatryLock(waktu, satuan)
Dapat dibatalkan saat menungguTidaklockInterruptibly()
Opsi fairnessTidaknew ReentrantLock(true)
Biaya penulisanPaling ringkasPaling bertele-tele tapi paling lengkap
KecocokanKebanyakan wilayah kritisWilayah kritis dengan kebutuhan kendali khusus

Aturan memilihnya sederhana: mulai dari synchronized, dan pindah ke ReentrantLock hanya ketika satu dari tiga fitur tambahannya benar-benar dibutuhkan. Salah satu pola lanjutan yang hanya bisa dibangun dengan pustaka lock ini adalah pasangan kunci baca-tulis — pembaca bersamaan, penulis eksklusif — yang menjadi topik Chapter ReadWriteLock berikutnya.

Referensi #

Berkas Sumber

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

ReentrantLockDasar.java

Unduh berkas
java
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDasar {
    private static class Kas {
        private long saldo = 0;
        private final ReentrantLock kunci = new ReentrantLock();

        void setor(long jumlah) {
            kunci.lock(); // minta lock; menunggu bila sedang dipegang thread lain
            try {
                saldo += jumlah; // critical section
            } finally {
                kunci.unlock(); // WAJIB dilepas apa pun yang terjadi
            }
        }

        long bacaSaldo() {
            kunci.lock();
            try {
                return saldo;
            } finally {
                kunci.unlock();
            }
        }
    }

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

TryLockTimeout.java

Unduh berkas
java
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class TryLockTimeout {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock kunci = new ReentrantLock();

        // Percobaan 0: tryLock() tanpa batas waktu saat lock masih bebas.
        boolean bebas = kunci.tryLock(); // langsung melaporkan true/false tanpa menunggu
        System.out.println("tryLock() langsung : " + bebas);
        if (bebas) {
            kunci.unlock();
        }

        // Pemegang lock akan menahan lock selama 300 milidetik.
        Thread pemegang = new Thread(() -> {
            kunci.lock();
            try {
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            } finally {
                kunci.unlock();
            }
        }, "pemegang");
        pemegang.start();
        Thread.sleep(50); // pastikan pemegang sudah memegang lock

        // Percobaan 1: batas waktu 50 ms -- lock baru lepas di sekitar ms ke-300.
        boolean suksesCepat = kunci.tryLock(50, TimeUnit.MILLISECONDS);
        if (suksesCepat) {
            kunci.unlock(); // kebetulan berhasil: lepaskan kembali
        } else {
            System.out.println("tryLock(50 ms)     : gagal (lock masih dipegang)");
        }

        // Percobaan 2: batas waktu 1 detik -- menunggu sampai pemegang selesai.
        boolean suksesSabar = kunci.tryLock(1, TimeUnit.SECONDS);
        System.out.println("tryLock(1 detik)   : " + (suksesSabar ? "berhasil" : "gagal"));
        if (suksesSabar) {
            kunci.unlock();
        }

        pemegang.join();
    }
}