Mengelola Thread Pool di Java: Panduan Langkah demi Langkah
Mengelola thread pool di Java berarti menentukan berapa banyak thread yang boleh berjalan bersamaan, bagaimana tugas antre, dan apa yang terjadi saat antrean penuh. Panduan ini membandingkan pilihan ExecutorService yang tersedia dan menunjukkan langkah konkret mengaturnya untuk aplikasi server.
Kapan thread pool perlu diatur manual
Thread pool bawaan seperti Executors.newFixedThreadPool(10) cukup untuk tugas sederhana. Pada aplikasi web dengan beban tidak merata, antrean tak terbatas di dalamnya bisa menimbun tugas sampai memori habis. Gejalanya muncul sebagai respons yang melambat dan log penuh peringatan OutOfMemoryError: unable to create new native thread. Di titik itu, konfigurasi manual mulai diperlukan.
Langkah 1: Pilih jenis ExecutorService yang sesuai
Java menyediakan beberapa implementasi dengan perilaku antrean berbeda. Membandingkannya membantu menghindari pilihan yang tampak mudah tetapi menyulitkan saat beban naik.
| Jenis | Perilaku antrean | Cocok untuk |
|---|---|---|
| newFixedThreadPool | Antrean tak terbatas | Tugas pendek dengan lonjakan rendah |
| newCachedThreadPool | Thread baru saat dibutuhkan, idle dibuang | Tugas singkat dan banyak |
| newScheduledThreadPool | Antrean prioritas waktu | Tugas berkala seperti pembersihan cache |
| ThreadPoolExecutor manual | Antrean berbatas dan kebijakan penolakan | Server produksi dengan beban tak terduga |
Untuk sebagian besar layanan internal, ThreadPoolExecutor dengan antrean berbatas lebih aman karena membatasi jumlah tugas yang menunggu. Jika beban berasal dari permintaan HTTP, pilihan web server juga memengaruhi ukuran pool yang wajar, seperti dibahas pada perbandingan web server Java untuk aplikasi intranet.
Langkah 2: Tentukan ukuran pool berdasarkan jenis tugas
Rumus umum untuk tugas yang terikat CPU adalah jumlah inti ditambah satu. Untuk tugas yang menunggu I/O, ukuran bisa lebih besar karena thread banyak menghabiskan waktu menunggu. Ukur dulu waktu tunggu rata-rata dan waktu proses di lingkungan yang mendekati produksi. Jika rasio tunggu terhadap proses sekitar 10 banding 1, pool berukuran beberapa kali jumlah inti masih masuk akal.
Hindari menetapkan angka besar tanpa dasar. Thread yang menganggur tetap memakan memori tumpukan, dan perpindahan konteks berlebihan menurunkan throughput. Mulai dari perkiraan konservatif, lalu naikkan bertahap sambil memantau waktu respons dan penggunaan CPU.
Langkah 3: Susun ThreadPoolExecutor dengan parameter eksplisit
Konstruktor ThreadPoolExecutor menerima ukuran inti, ukuran maksimum, waktu hidup thread idle, satuan waktu, antrean, thread factory, dan handler penolakan. Contoh berikut memakai antrean berbatas dan kebijakan pemanggil yang menjalankan tugas di thread pemanggil saat antrean penuh, sehingga tekanan balik terasa di sisi pemanggil.
import java.util.concurrent.*;
public class PoolContoh {
public static void main(String[] args) {
int inti = 4;
int maks = 8;
BlockingQueue<Runnable> antrean = new ArrayBlockingQueue<>(100);
ThreadFactory pabrik = r -> {
Thread t = new Thread(r);
t.setName("tugas-" + t.getId());
return t;
};
RejectedExecutionHandler penolakan = new ThreadPoolExecutor.CallerRunsPolicy();
ThreadPoolExecutor pool = new ThreadPoolExecutor(
inti, maks, 60L, TimeUnit.SECONDS,
antrean, pabrik, penolakan
);
for (int i = 0; i < 20; i++) {
final int nomor = i;
pool.execute(() -> System.out.println("Tugas " + nomor +
" di " + Thread.currentThread().getName()));
}
pool.shutdown();
}
}
CallerRunsPolicy membuat pemanggil ikut mengeksekusi tugas saat antrean penuh. Efeknya, laju permintaan baru melambat secara alami. Kebijakan lain seperti AbortPolicy melempar pengecualian dan perlu ditangani di lapisan pemanggil.
Langkah 4: Tangani hasil dan pengecualian tugas
Tugas yang dijalankan lewat execute tidak mengembalikan nilai. Jika perlu hasil, gunakan submit yang mengembalikan Future. Panggilan get pada Future akan melempar ExecutionException bila tugas gagal, sehingga penanganan kesalahan tidak hilang diam-diam. Untuk banyak tugas sekaligus, invokeAll mengumpulkan semua hasil dalam satu daftar.
Bungkus tugas yang rawan gagal dengan blok try-catch di dalam Runnable bila kegagalan tidak boleh menghentikan alur utama. Log kesalahan di dalam tugas, bukan hanya di pemanggil, agar jejaknya jelas.
Langkah 5: Pantau dan sesuaikan ukuran pool
Tanpa pemantauan, perubahan beban tidak terlihat sampai aplikasi melambat. ThreadPoolExecutor menyediakan metode seperti getActiveCount, getQueue().size(), dan getCompletedTaskCount. Ekspos nilai ini ke sistem metrik internal agar trennya terlihat. Jika antrean sering mendekati batas sementara CPU masih longgar, tambah ukuran pool secara bertahap. Jika CPU penuh dan antrean tetap panjang, masalahnya ada di kapasitas pemrosesan, bukan di jumlah thread.
Untuk aplikasi yang mengakses basis data dari dalam tugas, ukuran pool harus sejalan dengan jumlah koneksi yang tersedia. Konfigurasi pool koneksi yang terlalu kecil akan membuat thread saling menunggu, seperti dijelaskan pada panduan konfigurasi connection pool di Java.
Langkah 6: Matikan pool dengan benar saat aplikasi berhenti
Panggil shutdown untuk menolak tugas baru sambil menyelesaikan tugas yang sedang berjalan. Jika aplikasi membutuhkan penghentian cepat, shutdownNow mengirim interupsi ke thread yang aktif. Setelah itu, tunggu dengan awaitTermination selama batas waktu tertentu. Tanpa langkah ini, tugas yang tertunda bisa hilang atau membuat proses Java tidak berhenti dengan rapi, terutama saat deploy ulang di Tomcat seperti pada cara deploy aplikasi Java ke Tomcat.
Ringkasan
Mengelola thread pool di Java berarti memilih jenis executor yang sesuai, menetapkan ukuran berdasarkan sifat tugas, memakai antrean berbatas, menangani hasil dan pengecualian, memantau metrik, serta mematikan pool dengan rapi. Langkah-langkah ini mengurangi risiko kehabisan memori dan menjaga waktu respons tetap terkendali saat beban berubah.
Pertanyaan yang Sering Diajukan
Berapa ukuran thread pool yang ideal untuk aplikasi web Java?
Tidak ada angka tunggal. Untuk tugas yang banyak menunggu I/O, pool bisa beberapa kali jumlah inti CPU. Untuk tugas komputasi berat, jumlah inti ditambah satu biasanya cukup. Ukur waktu tunggu dan waktu proses di lingkungan yang mendekati produksi, lalu sesuaikan bertahap sambil memantau antrean dan penggunaan CPU.
Apa beda execute dan submit pada ExecutorService?
execute menerima Runnable dan tidak mengembalikan nilai, sehingga pengecualian yang tidak tertangani bisa hilang. submit mengembalikan Future yang membawa hasil atau pengecualian, sehingga kegagalan tugas dapat diperiksa lewat get.
Apakah thread pool perlu dimatikan saat aplikasi berhenti?
Perlu. Tanpa shutdown atau shutdownNow, thread non-daemon dapat menahan proses Java tetap hidup. Saat deploy ulang di server aplikasi, pool yang tidak dimatikan juga bisa meninggalkan tugas lama yang masih berjalan.






