Seluruh bab sebelumnya melindungi data bersama dengan lock: synchronized, ReentrantLock, ReadWriteLock. Pendekatan itu benar, tetapi menuntut disiplin — satu method yang lupa mengunci cukup untuk meruntuhkan jaminan. Pustaka java.util.concurrent menawarkan jalan lain: struktur data yang dirancang konkuren sejak fondasinya, sehingga put, get, dan iterasi dari banyak thread aman tanpa satu baris synchronized pun dari sisi pemakai. Bab ini membedah empat warga utamanya — ConcurrentHashMap, CopyOnWriteArrayList, ConcurrentSkipListMap, dan keluarga BlockingQueue — beserta kapan masing-masing menjadi pilihan yang tepat dibanding wrapper Collections.synchronizedMap dan kawan-kawannya.
ConcurrentHashMap: Operasi Atomik Per Entri #
ConcurrentHashMap tidak mengunci seluruh peta saat satu thread menulis. Implementasi modernnya mengombinasikan operasi compare-and-swap (CAS) untuk kasus tanpa bentrokan dan penguncian per-bin (keranjang hash) bila bentrokan terjadi — dua thread yang menyentuh kunci berbeda hampir tidak saling mengganggu. Generasi awal memakai lock striping (16 segmen terkunci terpisah); desain kini lebih halus lagi, tetapi gagasan intinya sama: zona penguncian sekecil mungkin.
Dampaknya paling terasa pada operasi majemuk yang biasanya berisiko check-then-act: merge, computeIfAbsent, dan putIfAbsent dijamin berjalan atomik:
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class PetaKonkuren {
public static void main(String[] args) throws Exception {
Map<String, Integer> hitungKata = new ConcurrentHashMap<>();
List<List<String>> dokumen = List.of(
List.of("kopi", "teh", "kopi"),
List.of("teh", "susu", "teh"),
List.of("kopi", "susu", "jus"));
// Tiga pekerja menghitung kata pada peta yang SAMA tanpa lock manual:
// merge() menambah nilai secara atomik per entri.
try (ExecutorService eksekutor = Executors.newFixedThreadPool(3)) {
for (List<String> kataDokumen : dokumen) {
eksekutor.submit(() -> {
for (String kata : kataDokumen) {
hitungKata.merge(kata, 1, Integer::sum);
}
});
}
} // try-with-resources: menunggu seluruh task selesai
System.out.println("jumlah kata (terurut menurut alfabet):");
for (Map.Entry<String, Integer> e : new TreeMap<>(hitungKata).entrySet()) {
System.out.println(" " + e.getKey() + ": " + e.getValue());
}
// computeIfAbsent pada ConcurrentHashMap bersifat atomik: fungsi
// pembuat nilai dijamin dijalankan tepat SATU kali meski dikejar
// banyak thread yang meminta kunci yang sama bersamaan.
Map<String, Integer> cacheKonfigurasi = new ConcurrentHashMap<>();
AtomicInteger pemanggilanLoader = new AtomicInteger();
try (ExecutorService eksekutor = Executors.newFixedThreadPool(3)) {
for (int i = 0; i < 3; i++) {
eksekutor.submit(() -> {
cacheKonfigurasi.computeIfAbsent("katalog", kunci -> {
pemanggilanLoader.incrementAndGet(); // penghitung pemanggilan
return 42; // nilai "dimuat dari sumber luar"
});
});
}
}
System.out.println("loader katalog dijalankan: " + pemanggilanLoader.get() + " kali");
System.out.println("nilai katalog : " + cacheKonfigurasi.get("katalog"));
}
}Output yang Dihasilkan:
jumlah kata (terurut menurut alfabet):
jus: 1
kopi: 3
susu: 2
teh: 3
loader katalog dijalankan: 1 kali
nilai katalog : 42Mari kita bedah dua operasi kuncinya:
hitungKata.merge(kata, 1, Integer::sum): baca-jumlah-tulis terjadi sebagai satu kesatuan. Tiga thread menambahkopihampir bersamaan dan hasilnya tetap3— tanpamerge, polagetlaluputberisiko kehilangan pembaruan (masalah lost update dari Chapter Race Conditions).computeIfAbsent("katalog", ...)dikejar tiga thread: fungsi pembuat nilai dijalankan tepat satu kali — penghitung mencatat1. Thread kedua dan ketiga yang datang kemudian menemukan entri sudah ada dan langsung memakai nilai42. Properti ini menjadikancomputeIfAbsentpola standar cache konkurensi; Javadoc menuntut fungsi pembuatnya singkat dan tidak menyentuh peta yang sama.- Pencetakan setelah
close()/join: hasil dicetak setelah seluruh task selesai;TreeMapsalinan mengurutkan kunci agar keluaran stabil alfabetis.
CopyOnWriteArrayList: Iterasi Salinan Beku #
ArrayList biasa punya iterator fail-fast: modifikasi struktural yang terjadi antara pembuatan iterator dan next() membuat ConcurrentModificationException terlempar. CopyOnWriteArrayList memilih jalan sebaliknya — setiap penulisan menyalin seluruh array dasar, dan iterator memegang salinan yang dibekukan saat iterator dibuat:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class SalinanSaatTulis {
public static void main(String[] args) throws InterruptedException {
// Bagian 1: CopyOnWriteArrayList -- iterator bekerja pada salinan
// array yang dibekukan SAAT iterator itu dibuat.
CopyOnWriteArrayList<String> namaTim =
new CopyOnWriteArrayList<>(List.of("Budi", "Siti", "Dewi"));
Iterator<String> pembaca = namaTim.iterator(); // salinan beku: 3 nama
// Penulisan terjadi SETELAH iterator dibuat -- penulis menambah
// anggota baru sementara iterator akan dibaca setelahnya.
Thread penulis = new Thread(() -> namaTim.add("Rina"), "penulis");
penulis.start();
penulis.join();
List<String> yangTerlihat = new ArrayList<>();
pembaca.forEachRemaining(yangTerlihat::add); // iterasi salinan lama
System.out.println("yang dilihat iterator : " + yangTerlihat);
System.out.println("isi terkini : " + namaTim);
// Bagian 2: ArrayList biasa dengan pola yang sama -- iterator
// fail-fast mendeteksi modifikasi struktural dan melempar CME.
List<String> daftarBiasa = new ArrayList<>(List.of("Budi", "Siti", "Dewi"));
Iterator<String> pembacaBiasa = daftarBiasa.iterator();
daftarBiasa.add("Rina"); // modifikasi struktural setelah iterator dibuat
try {
while (pembacaBiasa.hasNext()) {
pembacaBiasa.next();
}
System.out.println("iterasi ArrayList biasa selesai tanpa galat");
} catch (java.util.ConcurrentModificationException e) {
System.out.println("ArrayList biasa melempar ConcurrentModificationException");
}
}
}Output yang Dihasilkan:
yang dilihat iterator : [Budi, Siti, Dewi]
isi terkini : [Budi, Siti, Dewi, Rina]
ArrayList biasa melempar ConcurrentModificationExceptionTiga sifat yang dibuktikan keluarannya:
- Iterator melihat salinan lama: penulisan
"Rina"selesai sebelum iterasi dimulai, tetapi iterator tetap melaporkan tiga nama — salinannya dibekukan lebih awal, jauh sebelum penulis bekerja. Konsistensi semacam ini (snapshot isolation) ideal untuk listener daftar berlangganan dan tabel referensi yang jarang berubah. - Tidak ada
ConcurrentModificationException: penulisan tidak membatalkan iterator yang sedang berjalan karena keduanya tidak pernah menyentuh array yang sama. - Kontras
ArrayListbiasa: pola yang persis sama — iterator dibuat, daftar dimodifikasi,next()dipanggil — berakhir denganConcurrentModificationException. KelasArrayListsengaja memilih fail-fast agar bug modifikasi-bersamaan cepat terlihat, bukan disimpan diam-diam.
ConcurrentSkipListMap: Terurut dan Konkuren #
Peta hash tidak menjaga urutan kunci. Bila urutan penting — buku pesanan saham, indeks rentang waktu — pilihannya ConcurrentSkipListMap: implementasi skip list konkuren dengan operasi O(log n) yang iterasinya selalu terurut menaik:
import java.util.NavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
public class PetaTerurutKonkuren {
public static void main(String[] args) throws InterruptedException {
NavigableMap<Integer, String> bukuPenawaran = new ConcurrentSkipListMap<>();
// Dua penjual memasang penawaran pada rentang kunci yang berselang-seling;
// peta tetap terurut meski penulisan terjadi bersamaan.
Thread penjualA = new Thread(() -> {
for (int harga = 10; harga <= 50; harga += 10) {
bukuPenawaran.put(harga, "penawaran-A");
}
}, "penjual-A");
Thread penjualB = new Thread(() -> {
for (int harga = 15; harga <= 55; harga += 10) {
bukuPenawaran.put(harga, "penawaran-B");
}
}, "penjual-B");
penjualA.start();
penjualB.start();
penjualA.join();
penjualB.join();
// Setelah join, iterasi dijamin terurut menaik menurut kunci.
System.out.println("peta terurut : " + bukuPenawaran);
System.out.println("kunci terkecil : " + bukuPenawaran.firstKey());
System.out.println("kunci terbesar : " + bukuPenawaran.lastKey());
System.out.println("jumlah penawaran : " + bukuPenawaran.size());
// Operasi navigasi khas NavigableMap.
System.out.println("penawaran <= 22 : " + bukuPenawaran.floorKey(22));
System.out.println("penawaran >= 22 : " + bukuPenawaran.ceilingKey(22));
}
}Output yang Dihasilkan:
peta terurut : {10=penawaran-A, 15=penawaran-B, 20=penawaran-A, 25=penawaran-B, 30=penawaran-A, 35=penawaran-B, 40=penawaran-A, 45=penawaran-B, 50=penawaran-A, 55=penawaran-B}
kunci terkecil : 10
kunci terbesar : 55
jumlah penawaran : 10
penawaran <= 22 : 20
penawaran >= 22 : 25Dua catatan penting dari contoh tersebut:
- Rentang kunci dua penjual saling berselang tanpa bertabrakan: penjual-A menulis 10, 20, 30, 40, 50 dan penjual-B menulis 15, 25, 35, 45, 55. Setelah
join, peta berisi sepuluh entri terurut rapi — penjadwal bebas mengatur urutan penulisan tanpa mengubah isi akhir. floorKeydanceilingKey: navigasi “kunci terbesar ≤ x” dan “kunci terkecil ≥ x” — alat baku buku pesanan dan pencarian rentang. Kelas ini juga menjadi padanan konkuren untukTreeMap.
Keluarga BlockingQueue #
Interface BlockingQueue menutup daftar dengan pola yang sudah dipraktikkan pada Chapter wait, notify, dan notifyAll: put() menunggu bila penuh, take() menunggu bila kosong:
BlockingQueue<String> tugas = new ArrayBlockingQueue<>(100); // array melingkar, kapasitas tetap
BlockingQueue<String> antrian = new LinkedBlockingQueue<>(); // antrean berantai, opsional dibatasi
tugas.put("pesan-001"); // menunggu bila penuh
String item = tugas.take(); // menunggu bila kosongArrayBlockingQueue memakai satu array melingkar dengan satu pasang lock (produksi/konsumsi) — hemat dan prediktabel untuk kapasitas tetap. LinkedBlockingQueue berbasis simpul berantai dengan dua lock terpisah sehingga produsen dan konsumen jarang saling menunggu. PriorityBlockingQueue mengurutkan berdasarkan prioritas; SynchronousQueue tanpa penyangga menyerahkan item langsung dari produsen ke konsumen.
Memilih Collection Konkuren #
| Kebutuhan | Pilihan | Bandingkan dengan |
|---|---|---|
| Peta key-value padat akses | ConcurrentHashMap | Collections.synchronizedMap mengunci seluruh peta pada tiap operasi |
| Daftar kecil banyak dibaca | CopyOnWriteArrayList | Collections.synchronizedList memblokir iterasi seluruhnya |
| Peta terurut / navigasi rentang | ConcurrentSkipListMap | TreeMap + lock eksternal (rawan lupa) |
| Serah-terima antar thread | ArrayBlockingQueue / LinkedBlockingQueue | wait/notify manual |
| Set konkuren terurut | ConcurrentSkipListSet | TreeSet + lock eksternal |
Wrapper Collections.synchronized* masih sah dipakai — iterasinya saja yang harus disinkronkan manual lewat blok synchronized(koleksi). Untuk kode baru, collection konkuren di atas hampir selalu lebih cepat sekaligus lebih sulit salah. Struktur berikutnya melangkah dari penyimpanan ke komputasi: membelah pekerjaan besar menjadi tugas-tugas kecil yang saling mencuri — Fork/Join Framework.
Referensi #
- Oracle: Package java.util.concurrent (Java API Specification)
- Oracle: Class ConcurrentHashMap (Java API Specification)
- Oracle: Class CopyOnWriteArrayList (Java API Specification)
- Oracle: Class ConcurrentSkipListMap (Java API Specification)
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PetaKonkuren.java
Unduh berkasimport java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class PetaKonkuren {
public static void main(String[] args) throws Exception {
Map<String, Integer> hitungKata = new ConcurrentHashMap<>();
List<List<String>> dokumen = List.of(
List.of("kopi", "teh", "kopi"),
List.of("teh", "susu", "teh"),
List.of("kopi", "susu", "jus"));
// Tiga pekerja menghitung kata pada peta yang SAMA tanpa lock manual:
// merge() menambah nilai secara atomik per entri.
try (ExecutorService eksekutor = Executors.newFixedThreadPool(3)) {
for (List<String> kataDokumen : dokumen) {
eksekutor.submit(() -> {
for (String kata : kataDokumen) {
hitungKata.merge(kata, 1, Integer::sum);
}
});
}
} // try-with-resources: menunggu seluruh task selesai
System.out.println("jumlah kata (terurut menurut alfabet):");
for (Map.Entry<String, Integer> e : new TreeMap<>(hitungKata).entrySet()) {
System.out.println(" " + e.getKey() + ": " + e.getValue());
}
// computeIfAbsent pada ConcurrentHashMap bersifat atomik: fungsi
// pembuat nilai dijamin dijalankan tepat SATU kali meski dikejar
// banyak thread yang meminta kunci yang sama bersamaan.
Map<String, Integer> cacheKonfigurasi = new ConcurrentHashMap<>();
AtomicInteger pemanggilanLoader = new AtomicInteger();
try (ExecutorService eksekutor = Executors.newFixedThreadPool(3)) {
for (int i = 0; i < 3; i++) {
eksekutor.submit(() -> {
cacheKonfigurasi.computeIfAbsent("katalog", kunci -> {
pemanggilanLoader.incrementAndGet(); // penghitung pemanggilan
return 42; // nilai "dimuat dari sumber luar"
});
});
}
}
System.out.println("loader katalog dijalankan: " + pemanggilanLoader.get() + " kali");
System.out.println("nilai katalog : " + cacheKonfigurasi.get("katalog"));
}
}
PetaTerurutKonkuren.java
Unduh berkasimport java.util.NavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
public class PetaTerurutKonkuren {
public static void main(String[] args) throws InterruptedException {
NavigableMap<Integer, String> bukuPenawaran = new ConcurrentSkipListMap<>();
// Dua penjual memasang penawaran pada rentang kunci yang berselang-seling;
// peta tetap terurut meski penulisan terjadi bersamaan.
Thread penjualA = new Thread(() -> {
for (int harga = 10; harga <= 50; harga += 10) {
bukuPenawaran.put(harga, "penawaran-A");
}
}, "penjual-A");
Thread penjualB = new Thread(() -> {
for (int harga = 15; harga <= 55; harga += 10) {
bukuPenawaran.put(harga, "penawaran-B");
}
}, "penjual-B");
penjualA.start();
penjualB.start();
penjualA.join();
penjualB.join();
// Setelah join, iterasi dijamin terurut menaik menurut kunci.
System.out.println("peta terurut : " + bukuPenawaran);
System.out.println("kunci terkecil : " + bukuPenawaran.firstKey());
System.out.println("kunci terbesar : " + bukuPenawaran.lastKey());
System.out.println("jumlah penawaran : " + bukuPenawaran.size());
// Operasi navigasi khas NavigableMap.
System.out.println("penawaran <= 22 : " + bukuPenawaran.floorKey(22));
System.out.println("penawaran >= 22 : " + bukuPenawaran.ceilingKey(22));
}
}
SalinanSaatTulis.java
Unduh berkasimport java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class SalinanSaatTulis {
public static void main(String[] args) throws InterruptedException {
// Bagian 1: CopyOnWriteArrayList -- iterator bekerja pada salinan
// array yang dibekukan SAAT iterator itu dibuat.
CopyOnWriteArrayList<String> namaTim =
new CopyOnWriteArrayList<>(List.of("Budi", "Siti", "Dewi"));
Iterator<String> pembaca = namaTim.iterator(); // salinan beku: 3 nama
// Penulisan terjadi SETELAH iterator dibuat -- penulis menambah
// anggota baru sementara iterator akan dibaca setelahnya.
Thread penulis = new Thread(() -> namaTim.add("Rina"), "penulis");
penulis.start();
penulis.join();
List<String> yangTerlihat = new ArrayList<>();
pembaca.forEachRemaining(yangTerlihat::add); // iterasi salinan lama
System.out.println("yang dilihat iterator : " + yangTerlihat);
System.out.println("isi terkini : " + namaTim);
// Bagian 2: ArrayList biasa dengan pola yang sama -- iterator
// fail-fast mendeteksi modifikasi struktural dan melempar CME.
List<String> daftarBiasa = new ArrayList<>(List.of("Budi", "Siti", "Dewi"));
Iterator<String> pembacaBiasa = daftarBiasa.iterator();
daftarBiasa.add("Rina"); // modifikasi struktural setelah iterator dibuat
try {
while (pembacaBiasa.hasNext()) {
pembacaBiasa.next();
}
System.out.println("iterasi ArrayList biasa selesai tanpa galat");
} catch (java.util.ConcurrentModificationException e) {
System.out.println("ArrayList biasa melempar ConcurrentModificationException");
}
}
}