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:
saldo akhir: 200000Empat hal patut dicermati dari pola tersebut:
- Baris 8 (
private final ReentrantLock kunci = ...): lock disimpan sebagai fieldprivate final— disiplin yang sama dengan objek lock pada Chapter synchronized, agar tidak ada kode luar yang bisa mengunci jalur kita. - Baris 11-15 (
lock()-try-finally-unlock()):unlock()berada difinallysehingga tetap terpanggil bilasaldo += jumlahmelempar pengecualian. Catatan penting: hanya thread pemegang lock yang boleh memanggilunlock(); pemanggil dari thread lain melemparIllegalMonitorStateException. - Sifat reentrant: nama class bukan hiasan. Thread yang sudah memegang lock boleh memanggil
lock()lagi — misalnya ketikasetor()memanggil method lain yang mengunci lock yang sama — dan harus mengimbangi denganunlock()sebanyak itu. - Baris 29-30 (dua thread): hasil
200000yang selalu benar menegaskanReentrantLockmemberi 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:
tryLock() langsung : true
tryLock(50 ms) : gagal (lock masih dipegang)
tryLock(1 detik) : berhasilKronologi program tersebut:
tryLock() langsung : true: lock masih bebas, jadi laporan datang seketika tanpa menunggu; hasilnya langsung dilepaskan kembali.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,tryLockmelaporkanfalsedan threadmainbebas melanjutkan hidup — tidak menggantung menunggu tanpa batas.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.- Pola
if (sukses) unlock(): setiaptryLockyang berhasil harus diimbangiunlock()— 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:
kunci.lockInterruptibly();
try {
// wilayah kritis
} finally {
kunci.unlock();
}
// di thread lain: pekerja.interrupt(); -- penantian dibatalkan, bukan menunggu abadiTanpa 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.
ReentrantLock cepat = new ReentrantLock(); // unfair (bawaan): throughput terbaik
ReentrantLock adil = new ReentrantLock(true); // fair: giliran menunggu terlamasynchronized atau ReentrantLock #
| Aspek | synchronized | ReentrantLock |
|---|---|---|
| Bentuk | Kata kunci bahasa | Objek dari pustaka java.util.concurrent.locks |
| Pelepasan lock | Otomatis saat blok/method berakhir | Manual — unlock() di finally |
| Batas waktu | Tidak ada | tryLock(waktu, satuan) |
| Dapat dibatalkan saat menunggu | Tidak | lockInterruptibly() |
| Opsi fairness | Tidak | new ReentrantLock(true) |
| Biaya penulisan | Paling ringkas | Paling bertele-tele tapi paling lengkap |
| Kecocokan | Kebanyakan wilayah kritis | Wilayah 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 #
- Oracle: Class ReentrantLock (Java API Specification)
- Oracle: Package java.util.concurrent.locks (Summary)
- Oracle: The Java Tutorials — Lock Objects
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
ReentrantLockDasar.java
Unduh berkasimport 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 berkasimport 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();
}
}