Saat dua thread memakai satu koleksi biasa, hasilnya tidak dapat dipercaya. Pembaruan pada HashMap bisa hilang karena dua thread menulis alamat bucket yang sama, dan iterasi ArrayList yang dimodifikasi di tengah jalan memunculkan ConcurrentModificationException. Paket java.util.concurrent menyediakan koleksi yang dirancang untuk dipakai bersama dari awal. Topik ini membahas tiga yang paling sering dipakai, yaitu ConcurrentHashMap untuk peta bersama, CopyOnWriteArrayList untuk daftar yang jarang ditulis, dan BlockingQueue untuk serah terima tugas antar-thread.
ConcurrentHashMap untuk peta bersama#
ConcurrentHashMap menggantikan HashMap pada kode yang diakses banyak thread. Pengunciannya halus, yaitu thread yang menulis ke satu bucket tidak mengunci seluruh peta, sehingga pembacaan dan penulisan di bagian lain tetap berjalan. Yang lebih penting, kelas ini menyediakan operasi atomik seperti merge dan compute, yang melakukan baca-ubah-tulis dalam satu langkah terlindungi, persis persoalan yang sebelumnya ditangani synchronized secara manual.
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class DemoPetaKonkuren {
static Map<String, Integer> hitungan = new ConcurrentHashMap<>();
static void hitungKata(String[] bagian) {
for (String kata : bagian) {
hitungan.merge(kata, 1, Integer::sum);
}
}
public static void main(String[] args) throws InterruptedException {
String[] bagianA = {"java", "thread", "java", "map"};
String[] bagianB = {"java", "map", "java", "java"};
Thread t1 = new Thread(() -> hitungKata(bagianA));
Thread t2 = new Thread(() -> hitungKata(bagianB));
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("java : " + hitungan.get("java"));
System.out.println("map : " + hitungan.get("map"));
System.out.println("thread: " + hitungan.get("thread"));
}
}java : 5
map : 2
thread: 1Dua thread menghitung dua bagian daftar kata, dan kata java serta map disentuh keduanya. Pemanggilan merge(kata, 1, Integer::sum) menambah satu bila kata belum ada, atau menjumlahkan nilai lamanya bila sudah, dan seluruh langkah itu atomik sehingga tidak ada perhitungan yang hilang. Pada HashMap biasa dengan penambahan manual, sebagian kenaikan justru lenyap seperti yang disaksikan pada topik sinkronisasi. Satu batasan perlu diingat, kunci maupun nilai pada ConcurrentHashMap tidak boleh null.
CopyOnWriteArrayList untuk daftar yang jarang berubah#
CopyOnWriteArrayList memilih taktik yang berbeda, yaitu setiap penulisan membuat salinan array baru, mengubah salinannya, lalu menukarnya dengan array lama dalam satu operasi yang aman dibaca. Iterator yang sedang berjalan memegang array lama sebagai snapshot, yakni potret daftar pada saat iterator dibuat, sehingga daftar boleh diubah sekalipun sedang diiterasi. Biayanya jelas, menulis mahal karena menyalin seluruh isi, sehingga kelas ini pas untuk daftar yang dibaca sering dan diubah jarang, misalnya daftar pemantau atau daftar piket.
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class DemoCOW {
public static void main(String[] args) {
List<String> daftarPiket = new CopyOnWriteArrayList<>();
daftarPiket.add("Ali");
daftarPiket.add("Budi");
daftarPiket.add("Citra");
for (String nama : daftarPiket) {
System.out.println("Mengingatkan: " + nama);
daftarPiket.remove(nama);
}
System.out.println("Sisa list : " + daftarPiket);
}
}Mengingatkan: Ali
Mengingatkan: Budi
Mengingatkan: Citra
Sisa list : []Kode di atas menghapus elemen pada saat iterasi berjalan, sesuatu yang pada ArrayList biasa memicu ConcurrentModificationException. Iterator CopyOnWriteArrayList mengabaikan perubahan yang terjadi setelah ia dibuat, sehingga perulangannya menyelesaikan potret awalnya dengan tenang sampai tiga nama habis diingatkan. Perilaku snapshot ini juga berarti iterator tidak pernah melihat penambahan yang dilakukan thread lain di tengah jalan, dan hal itu perlu diantisipasi pada desainnya.
BlockingQueue untuk serah terima tugas#
BlockingQueue adalah antrean yang dibekali kemampuan menunggu. Metode put menunggu sampai antrean penuhnya punya ruang, sedangkan take menunggu sampai ada elemen yang bisa diambil, sehingga produsen dan konsumen saling mengatur kecepatannya tanpa wait dan notify manual. Kapasitas antrean yang dibatasi, misalnya ArrayBlockingQueue berkapasitas dua, membuat produsen yang terlalu cepat ikut melambat, dan itulah mekanisme penahan beban yang sehat.
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class DemoAntrian {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> antrian = new ArrayBlockingQueue<>(2);
Thread produsen = new Thread(() -> {
try {
for (int i = 1; i <= 3; i++) {
antrian.put("tiket-" + i);
}
antrian.put("SELESAI");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread konsumen = new Thread(() -> {
try {
while (true) {
String tugas = antrian.take();
if (tugas.equals("SELESAI")) {
break;
}
System.out.println("Diproses: " + tugas);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
produsen.start();
konsumen.start();
produsen.join();
konsumen.join();
System.out.println("Antrian kosong, program berakhir");
}
}Diproses: tiket-1
Diproses: tiket-2
Diproses: tiket-3
Antrian kosong, program berakhirProdusen menaruh tiga tiket lalu satu sinyal khusus, dan sinyal berhenti semacam itu dikenal sebagai poison pill, yaitu nilai yang sengaja disisipkan agar konsumen tahu pekerjaannya selesai. Konsumen berputar mengambil elemen, memprosesnya, dan berhenti tepat ketika sinyalnya muncul. Karena antrean berperilaku masuk-pertama-keluar-pertama dan konsumennya satu, urutan keluarannya selalu rapi meski kedua thread bergantian berebut waktu prosesor.
Kesalahan umum#
- Memakai
HashMapatauArrayListbiasa untuk data lintas-thread. Pembaruan yang hilang dan exception saat iterasi adalah akibatnya. Pilih koleksi dari paketjava.util.concurrentsesuai pola baca-tulisnya. - Mengira
size()dan operasi kumpulan padaConcurrentHashMapadalah satu potret. Nilainya konsisten per pemanggilan, tetapi menghitung lalu memutuskan berdasarkan hasilnya bukan langkah atomik. Gunakanmerge,compute, atauputIfAbsentuntuk keputusan yang harus utuh. - Memakai
CopyOnWriteArrayListpada daftar yang sering ditulis. Setiap penulisan menyalin seluruh array, sehingga beban penulisan yang tinggi membuat program melambat sendiri. Kelas ini milik pola baca-sering. - Memakai antrean tanpa batas untuk tugas dari pihak luar.
LinkedBlockingQueuetanpa kapasitas membuat antrean membengkak ketika konsumen kalah cepat. Batasi kapasitasnya agar tekanan terlihat dan produsen ikut menunggu.
Ringkasan#
ConcurrentHashMapaman dipakai bersama dan menyediakan operasi atomik sepertimergesertacomputeuntuk baca-ubah-tulis, tanpa menerimanull.CopyOnWriteArrayListmenyalin array pada setiap tulisan, dan iteratornya membaca snapshot sehingga aman sambil dimodifikasi.BlockingQueuemengatur kecepatan produsen dan konsumen lewatputsertatakeyang menunggu, dan kapasitasnya sebaiknya dibatasi.- Koleksi konkuren menutup persoalan yang sebelumnya ditangani
synchronizedmanual, dengan jaminan yang sudah diuji.
Referensi#
- Class ConcurrentHashMap — Oracle
— dokumentasi resmi, termasuk kontrak operasi atomik
mergedancompute. - Interface BlockingQueue — Oracle
— dokumentasi resmi perilaku
putdantakeyang menunggu. - Class CopyOnWriteArrayList — Oracle — dokumentasi resmi perilaku snapshot iteratornya.