Membuat Thread baru untuk setiap tugas terasa mudah pada program kecil, tetapi menjadi mahal ketika jumlah tugasnya besar, karena setiap thread membawa biaya memori dan waktu pembuatan yang tidak kecil. Framework executor menjawabnya dengan memisahkan pengiriman tugas dari eksekusinya, yaitu tugas dititipkan kepada ExecutorService yang memegang sekelompok thread yang hidup terus. Topik ini membahas konsep thread pool, perbedaan Runnable dan Callable, cara membaca hasil lewat Future, serta disiplin menutup executor agar program berhenti dengan rapi.
Konsep thread pool#
Thread pool adalah kumpulan thread yang dibuat sekali lalu dipakai berulang. Tugas yang dikirim tidak mendapat thread baru, melainkan diletakkan di antrean, dan thread yang sedang bebas mengambil tugas berikutnya dari antrean itu. Biaya pembuatan thread dibayar sekali di awal, jumlah thread yang hidup tetap terkendali, dan tugas yang datang gelombang tidak langsung membanjiri sistem. Alur kerjanya digambarkan berikut ini.
Kelas pabrik Executors menyediakan beberapa konfigurasi siap pakai. newFixedThreadPool(n) memelihara tepat n thread, newCachedThreadPool() menambah thread baru bila antrean menumpuk dan memakai ulang yang menganggur, sedangkan newSingleThreadExecutor() mengeksekusi satu tugas pada satu waktu dengan urutan yang terjaga. Pada kode nyata, pembuatan pool sebaiknya dilakukan sekali dan dibagikan, bukan dibuat ulang untuk setiap permintaan.
Callable dan Future#
Interface Runnable tidak mengembalikan nilai dan tidak boleh melempar checked exception, sehingga kurang pas untuk tugas yang hasilnya dibutuhkan pemanggil. Untuk kebutuhan itu tersedia Callable<V>, yaitu tugas yang mengembalikan nilai bertipe V dan boleh melempar exception. Metode submit mengembalikan Future<V>, yaitu tanda terima yang menampung hasil tugas, lengkap dengan metode get() yang menunggu sampai hasilnya tersedia dan membawa exception tugas ke pemanggilnya bila gagal.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class DemoExecutor {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> jumlahDigit = pool.submit(() -> hitungDigit("piket 21 nov 2026"));
Future<Integer> jumlahKata = pool.submit(() -> hitungKata("belajar konkurensi java"));
System.out.println("Dua tugas dikirim, main tidak menunggu sambil berdiri");
System.out.println("Jumlah digit: " + jumlahDigit.get());
System.out.println("Jumlah kata : " + jumlahKata.get());
pool.shutdown();
System.out.println("Pool diminta berhenti menerima tugas baru");
}
static int hitungDigit(String teks) {
int total = 0;
for (char c : teks.toCharArray()) {
if (Character.isDigit(c)) {
total++;
}
}
return total;
}
static int hitungKata(String teks) {
return teks.trim().split("\\s+").length;
}
}Dua tugas dikirim, main tidak menunggu sambil berdiri
Jumlah digit: 6
Jumlah kata : 3
Pool diminta berhenti menerima tugas baruBaris pertama keluaran membuktikan sifat submit, yaitu tugas hanya dititipkan dan main langsung lanjut mengeksekusi baris berikutnya. Dua lambda bertipe Callable<Integer> dijalankan thread pool, dan hasil barunya diambil lewat get() yang menunggu selesainya tugas masing-masing. Bila salah satu lambda melempar exception, exception itu tidak muncul saat tugasnya gagal, melainkan dibungkus ExecutionException dan dilempar tepat pada pemanggilan get()-nya.
Menutup executor dengan disiplin#
Thread pada pool bukan daemon, sehingga pool yang tidak pernah ditutup membuat JVM tidak pernah berhenti. Penutupan yang disiplin bergerak dalam tiga langkah. Pertama, shutdown() menghentikan penerimaan tugas baru sementara tugas lama tetap diselesaikan. Kedua, awaitTermination menunggu dengan batas waktu. Ketiga, bila waktu habis, shutdownNow() mengembalikan daftar tugas yang belum sempat jalan dan berusaha menghentikan tugas yang sedang jalan lewat interupsi.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class DemoShutdown {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(2);
List<Future<String>> hasil = new ArrayList<>();
for (int i = 1; i <= 4; i++) {
int nomor = i;
hasil.add(pool.submit(() -> "Tugas " + nomor + " diproses"));
}
pool.shutdown();
boolean tertutup = pool.awaitTermination(5, TimeUnit.SECONDS);
System.out.println("Pool tertutup dalam waktu? " + tertutup);
for (Future<String> f : hasil) {
System.out.println(f.get());
}
}
}Pool tertutup dalam waktu? true
Tugas 1 diproses
Tugas 2 diproses
Tugas 3 diproses
Tugas 4 diprosesEmpat tugas dikirim dengan satu perulangan, dan hasilnya dikumpulkan ke daftar Future sebelum pool ditutup. Pencetakan dilakukan setelah awaitTermination mengonfirmasi bahwa pekerjaan sudah tuntas, sehingga urutan keluarannya rapi meski eksekusinya berselang-seling di dalam pool. Idiom penutupan tiga langkah yang lazim dipakai pada aplikasi nyata dituliskan sebagai berikut.
// tidak-dijalankan
pool.shutdown();
try {
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
pool.shutdownNow();
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("Pool tidak mau berhenti");
}
}
} catch (InterruptedException e) {
pool.shutdownNow();
Thread.currentThread().interrupt();
}Kesalahan umum#
- Membuat pool baru untuk setiap permintaan.
Executors.newFixedThreadPool(2)di dalam metode yang dipanggil berkali-kali menghasilkan pool yang terus bertambah tanpa penutupan. Buat pool sekali, bagikan, tutup saat aplikasi berhenti. - Memanggil
get()di tengah alur padahal tugasnya mandiri.Future.get()memblokir, sehingga mengirim lima tugas lalu langsungget()satu per satu membuat pekerjaan kembali berurutan. Kumpulkan semuaFuturedahulu, baru ambil hasilnya. - Menelan exception tugas tanpa sadar. Exception pada
Runnableyang disubmit tidak tampil di log, melainkan tersimpan diFuture-nya. Bacaget()atau tangani lewat pemantauan agar kegagalan tidak senyap. - Memanggil tugas tambahan setelah
shutdown(). Pengirimannya ditolak denganRejectedExecutionException, sehingga alur pengiriman tugas harus selesai sebelum penutupan dimulai.
Ringkasan#
ExecutorServicememisahkan pengiriman tugas dari eksekusinya, dengan thread yang hidup terus dan mengambil tugas dari antrean.Callable<V>mengembalikan nilai dan boleh melempar exception, sedangkansubmitmembungkus hasilnya dalamFutureyang dibaca lewatget().- Penutupan yang disiplin bergerak dari
shutdown(),awaitTerminationdengan batas waktu, hinggashutdownNow()bila waktu habis. - Exception tugas yang disubmit tersembunyi di dalam
Future-nya dan hanya muncul saatget()dipanggil.
Referensi#
- Interface ExecutorService — Oracle
— dokumentasi resmi
submit,shutdown,shutdownNow, danawaitTermination. - Executors — Oracle — bagian tutorial resmi tentang thread pool dan pemilihan ukurannya.
- Interface Callable — Oracle
— dokumentasi resmi
Callablesebagai tugas yang mengembalikan nilai.