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:

text
jumlah kata (terurut menurut alfabet):
  jus: 1
  kopi: 3
  susu: 2
  teh: 3
loader katalog dijalankan: 1 kali
nilai katalog            : 42

Mari kita bedah dua operasi kuncinya:

  1. hitungKata.merge(kata, 1, Integer::sum): baca-jumlah-tulis terjadi sebagai satu kesatuan. Tiga thread menambah kopi hampir bersamaan dan hasilnya tetap 3 — tanpa merge, pola get lalu put berisiko kehilangan pembaruan (masalah lost update dari Chapter Race Conditions).
  2. computeIfAbsent("katalog", ...) dikejar tiga thread: fungsi pembuat nilai dijalankan tepat satu kali — penghitung mencatat 1. Thread kedua dan ketiga yang datang kemudian menemukan entri sudah ada dan langsung memakai nilai 42. Properti ini menjadikan computeIfAbsent pola standar cache konkurensi; Javadoc menuntut fungsi pembuatnya singkat dan tidak menyentuh peta yang sama.
  3. Pencetakan setelah close()/join: hasil dicetak setelah seluruh task selesai; TreeMap salinan 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:

text
yang dilihat iterator : [Budi, Siti, Dewi]
isi terkini           : [Budi, Siti, Dewi, Rina]
ArrayList biasa melempar ConcurrentModificationException

Tiga sifat yang dibuktikan keluarannya:

  1. 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.
  2. Tidak ada ConcurrentModificationException: penulisan tidak membatalkan iterator yang sedang berjalan karena keduanya tidak pernah menyentuh array yang sama.
  3. Kontras ArrayList biasa: pola yang persis sama — iterator dibuat, daftar dimodifikasi, next() dipanggil — berakhir dengan ConcurrentModificationException. Kelas ArrayList sengaja 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:

text
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  : 25

Dua catatan penting dari contoh tersebut:

  1. 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.
  2. floorKey dan ceilingKey: navigasi “kunci terbesar ≤ x” dan “kunci terkecil ≥ x” — alat baku buku pesanan dan pencarian rentang. Kelas ini juga menjadi padanan konkuren untuk TreeMap.

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:

java
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 kosong

ArrayBlockingQueue 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 #

KebutuhanPilihanBandingkan dengan
Peta key-value padat aksesConcurrentHashMapCollections.synchronizedMap mengunci seluruh peta pada tiap operasi
Daftar kecil banyak dibacaCopyOnWriteArrayListCollections.synchronizedList memblokir iterasi seluruhnya
Peta terurut / navigasi rentangConcurrentSkipListMapTreeMap + lock eksternal (rawan lupa)
Serah-terima antar threadArrayBlockingQueue / LinkedBlockingQueuewait/notify manual
Set konkuren terurutConcurrentSkipListSetTreeSet + 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 #

Berkas Sumber

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

PetaKonkuren.java

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

PetaTerurutKonkuren.java

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

SalinanSaatTulis.java

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