Konkurensi: Thread, ExecutorService & CompletableFuture
Menjalankan beberapa tugas bersamaan dengan Thread, thread pool (ExecutorService), dan CompletableFuture, serta menghindari race condition dengan synchronized dan AtomicInteger.
Mengapa konkurensi?
Program biasa mengerjakan satu hal pada satu waktu. Padahal banyak tugas yang menunggu: mengunduh data, membaca berkas, query database. Dengan konkurensi, saat satu tugas menunggu, tugas lain tetap berjalan. Server web melayani ribuan pengguna dengan cara ini.
Setiap program Java punya minimal satu thread (utas eksekusi): thread main. Kita bisa membuat thread tambahan untuk pekerjaan lain.
Membuat Thread
Runnable tugas = () -> System.out.println("Berjalan di " + Thread.currentThread().getName());
Thread t = new Thread(tugas);
t.start(); // mulai thread baru (BUKAN run(), yang hanya memanggil biasa)
t.join(); // tunggu thread selesai
Runnable adalah interface fungsional dengan satu method run(), jadi bisa ditulis sebagai lambda.
Masalah: race condition
Bila dua thread mengubah variabel yang sama bersamaan, hasilnya bisa salah:
int hitung = 0; // dibagi oleh banyak thread
// hitung++ sebenarnya 3 langkah: baca, tambah, tulis.
// Dua thread bisa membaca nilai lama yang sama sehingga satu penambahan hilang.
Solusi paling sederhana adalah memakai class atomik:
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger hitung = new AtomicInteger();
hitung.incrementAndGet(); // aman antar thread
Alternatifnya synchronized: hanya satu thread pada satu waktu yang boleh masuk ke blok/method tersebut.
class Penghitung {
private int nilai;
public synchronized void tambah() { nilai++; }
public synchronized int ambil() { return nilai; }
}
ExecutorService: thread pool
Membuat thread sendiri-sendiri boros dan sulit diatur. Praktik standar: buat pool berisi sejumlah thread tetap, lalu serahkan tugas ke pool.
import java.util.concurrent.*;
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> hasil = pool.submit(() -> {
Thread.sleep(500); // pura-pura bekerja
return 42;
});
System.out.println(hasil.get()); // menunggu hasil (melempar InterruptedException, ExecutionException)
pool.shutdown(); // WAJIB: jika lupa, program tidak pernah berhenti
submit(Runnable)— tugas tanpa hasil.submit(Callable<T>)— tugas yang mengembalikan nilai; hasilnya dibungkusFuture<T>.Future.get()— memblokir sampai hasil siap.
CompletableFuture: tugas asinkron yang bisa dirangkai
import java.util.concurrent.CompletableFuture;
CompletableFuture<String> f = CompletableFuture
.supplyAsync(() -> "java") // jalan di thread lain
.thenApply(String::toUpperCase) // ubah hasilnya
.thenApply(s -> s + "!");
System.out.println(f.join()); // JAVA!
Menggabungkan dua tugas yang berjalan paralel:
CompletableFuture<Integer> a = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> b = CompletableFuture.supplyAsync(() -> 20);
int total = a.thenCombine(b, Integer::sum).join(); // 30
| Method | Fungsi |
|---|---|
supplyAsync(supplier) | Jalankan tugas yang mengembalikan nilai |
thenApply(fungsi) | Ubah hasil (seperti map) |
thenCombine(lain, fungsi) | Gabungkan dua hasil |
exceptionally(fungsi) | Nilai pengganti jika terjadi error |
join() | Tunggu hasil (tanpa checked exception) |
Virtual thread (Java 21)
Java 21 menambahkan virtual thread, thread yang sangat ringan sehingga bisa dibuat jutaan: Thread.startVirtualThread(tugas) atau Executors.newVirtualThreadPerTaskExecutor(). Cocok untuk server yang banyak menunggu I/O.
Aturan emas
- Lebih suka ExecutorService daripada
new Threadmanual. - Selalu panggil
shutdown()pada ExecutorService. - Data yang diubah banyak thread harus dilindungi (
Atomic*,synchronized) atau memakai struktur data aman sepertiConcurrentHashMap. - Lebih aman membagikan objek yang immutable (misalnya
record).
Error umum
- Program tidak berhenti setelah selesai → lupa
pool.shutdown(). - Hasil hitungan berbeda-beda tiap dijalankan → race condition; pakai
AtomicInteger. unreported exception InterruptedException→ bungkussleep/getdengan try-catch atau tambahkanthrows.