Membuat Thread satu per satu terasa wajar saat jumlahnya tiga, tetapi menjadi mimpi buruk ketika ratusan task datang setiap detik: siapa yang dibuat, siapa yang dimatikan, bagaimana membatasi jumlahnya agar server tidak tumbang, dan bagaimana mendapatkan hasil dari task yang tidak mengembalikan apa pun lewat Runnable. Framework Executor memisahkan dua keputusan itu dengan tegas: kode kita hanya mengirim apa yang dikerjakan (Runnable atau Callable), sedangkan ExecutorService yang memutuskan siapa dan berapa yang mengerjakan. Bab ini mempraktikkan pengiriman task, pengambilan hasil lewat Future, pengiriman sekelompok task lewat invokeAll/invokeAny, dan — sama pentingnya — proses mematikan eksekutor dengan benar.
Mengirim Task dan Mengambil Hasil #
Interface ExecutorService menerima dua jenis task yang sudah kita kenal dari Chapter Runnable dan Callable. Method submit() menerima keduanya: untuk Runnable, method tersebut mengembalikan Future<?> yang hanya melapor status selesai; untuk Callable<V>, Future<V> juga menyimpan nilai baliknya. Method get() pada Future memblokir pemanggil sampai task selesai lalu mengembalikan hasilnya:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class ExecutorServiceDasar {
public static void main(String[] args) throws Exception {
ExecutorService eksekutor = Executors.newFixedThreadPool(2);
try {
// Task 1: Callable<String> -- lambda mengembalikan nilai.
Future<String> tugasTeks = eksekutor.submit(
() -> "laporan penjualan siap");
// Task 2: Callable<Integer> -- penjumlahan 1..100 di thread pool.
Future<Integer> tugasHitung = eksekutor.submit(new Callable<Integer>() {
@Override
public Integer call() {
int total = 0;
for (int i = 1; i <= 100; i++) {
total += i;
}
return total;
}
});
// Task 3: Runnable -- aksi tanpa nilai balik; tanda selesai
// dikumpulkan lewat AtomicBoolean, bukan println dari dalam task.
AtomicBoolean arsipBerjalan = new AtomicBoolean(false);
Future<?> tugasAksi = eksekutor.submit(() -> {
arsipBerjalan.set(true); // lakukan pengarsipan...
});
// get() menunggu masing-masing task selesai, lalu hasil dicetak
// oleh main dalam urutan pemasukan -- deterministik.
System.out.println("hasil tugas teks : " + tugasTeks.get());
System.out.println("hasil tugas hitung : " + tugasHitung.get());
tugasAksi.get(); // tunggu Runnable selesai (mengembalikan null)
System.out.println("tugas aksi selesai : " + arsipBerjalan.get());
System.out.println("eksekutor masih hidup: " + !eksekutor.isTerminated());
} finally {
// Matikan eksekutor secara sopan: tolak task baru, tunggu task jalan.
eksekutor.shutdown();
if (!eksekutor.awaitTermination(5, TimeUnit.SECONDS)) {
eksekutor.shutdownNow(); // belum selesai setelah 5 detik: paksa
}
}
// Setelah shutdown, task baru pasti ditolak.
try {
eksekutor.submit(() -> System.out.println("tidak pernah berjalan"));
} catch (RejectedExecutionException e) {
System.out.println("task baru ditolak setelah shutdown");
}
System.out.println("eksekutor dimatikan: " + eksekutor.isTerminated());
}
}Output yang Dihasilkan:
hasil tugas teks : laporan penjualan siap
hasil tugas hitung : 5050
tugas aksi selesai : true
eksekutor masih hidup: true
task baru ditolak setelah shutdown
eksekutor dimatikan: trueMari kita bedah alurnya:
Executors.newFixedThreadPool(2): pabrik eksekutor berisi tepat dua thread pekerja. Tiga task yang dikirim akan dikerjakan dua-dua-dua — task ketiga menunggu di antrean internal sampai ada thread yang bebas.- Tiga bentuk
submit(): lambdaCallable(baris() -> "laporan..."), class anonimCallable<Integer>(bentuk eksplisit), dan lambdaRunnable(Future<?>). Ketiganya masuk antrean yang sama. - Tidak ada
printlndari dalam task: semua hasil dikumpulkan lewat nilai balik atauAtomicBoolean, lalumainmencetak setelahget()kembali. Dengan begitu urutan keluaran ditentukan oleh urutan pemanggilanget(), bukan oleh kecepatan thread. eksekutor masih hidup: true:isTerminated()masihfalsekarena eksekutor belum dimatikan — semua task selesai, tetapi pool masih siap menerima task baru.- Blok
finallydenganshutdown(): pola wajib.shutdown()menutup pintu masuk task baru dan membiarkan task berjalan menyelesaikan diri;awaitTermination(5, TimeUnit.SECONDS)menunggu sampai batas waktu;shutdownNow()adalah paksaan terakhir yang meng-interupsi thread yang masih bekerja.
Matikan dengan Cara Baru: try-with-resources #
Sejak Java 19, ExecutorService mengimplementasikan AutoCloseable. Bentuk try (ExecutorService e = ...) { ... } memanggil close() secara otomatis di akhir blok — close() menjalankan shutdown(), menunggu task selesai, lalu memaksa shutdownNow() bila terjadi interupsi. Untuk sebagian besar kasus, bentuk ini lebih ringkas dan tidak mungkin lupa mematikan.
invokeAll dan invokeAny #
Dua method pengelompokan menghindari loop submit() manual. invokeAll(kumpulanTask) mengirim seluruh task, menunggu semuanya selesai, lalu mengembalikan daftar Future yang urutannya mengikuti urutan pengiriman — bukan urutan penyelesaian. invokeAny(kumpulanTask) mengembalikan hasil task yang pertama berhasil, membatalkan sisanya, dan melempar ExecutionException bila semua task gagal:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class InvokeAllInvokeAny {
public static void main(String[] args) throws Exception {
// Bentuk modern sejak Java 19: try-with-resources -- close() akan
// mematikan eksekutor dan menunggu task berjalan sampai selesai.
try (ExecutorService eksekutor = Executors.newFixedThreadPool(3)) {
// invokeAll: kirim ketiga task, tunggu SEMUANYA selesai.
// Hasil Future terurut sesuai urutan pengiriman, bukan urutan selesai.
List<Callable<Integer>> daftarTugas = List.of(
() -> { Thread.sleep(80); return 10; }, // paling lambat
() -> 20, // tercepat
() -> { Thread.sleep(40); return 30; } // sedang
);
List<Future<Integer>> hasilAll = eksekutor.invokeAll(daftarTugas);
System.out.println("invokeAll -- hasil sesuai urutan pengiriman:");
for (int i = 0; i < hasilAll.size(); i++) {
System.out.println(" task ke-" + (i + 1) + ": " + hasilAll.get(i).get());
}
// invokeAny: kembali dengan hasil task yang PERTAMA berhasil.
// Ketiga task sengaja mengembalikan teks yang sama agar keluaran
// tidak bergantung pada task mana yang memenangkan perlombaan.
List<Callable<String>> pemeriksaan = new ArrayList<>();
for (int sumber = 1; sumber <= 3; sumber++) {
int jeda = sumber * 40; // tiap sumber punya kecepatan berbeda
pemeriksaan.add(() -> {
Thread.sleep(jeda);
return "stok tersedia";
});
}
String hasilAny = eksekutor.invokeAny(pemeriksaan);
System.out.println("invokeAny -- hasil: " + hasilAny);
}
}
}Output yang Dihasilkan:
invokeAll -- hasil sesuai urutan pengiriman:
task ke-1: 10
task ke-2: 20
task ke-3: 30
invokeAny -- hasil: stok tersediaTiga perilaku yang perlu diingat dari contoh tersebut:
- Urutan
invokeAlltetap stabil: task ke-1 paling lambat selesai (tidur 80 milidetik), tetapi hasilnya tetap tercetak pertama karena posisinya di daftar kembalian mengikuti daftar pengiriman. Sifat ini membuat keluaran contoh di atas deterministik meski ketiga thread berlari tak serempak. invokeAnyperlombaan pemenang tunggal: ketiga pemeriksaan mengecek tiga sumber stok dengan kecepatan berbeda; yang tercepat menang dan dua yang kalah dibatalkan. Karena ketiganya sengaja mengembalikan teks yang sama, keluaran tidak bergantung siapa pemenangnya — teknik yang tepat untuk contoh pembelajaran.Thread.sleepdi dalamCallable: meniru pekerjaan nyata (membaca berkas, memanggil layanan) yang memakan waktu bervariasi. Interupsi tetap terpropagasi lewatInterruptedException, sehinggashutdownNow()bekerja pada task yang sedang menunggu.
ExecutorService dengan begitu menyelesaikan dua pertanyaan besar: cara mengirim task dan cara mematikannya. Pertanyaan yang tersisa bersifat desain: pool seperti apa yang harus dipilih — fixed, cached, scheduled, atau work-stealing — dan berapa ukurannya. Chapter berikutnya memetakan pilihan tersebut.
Referensi #
- Oracle: Interface ExecutorService (Java API Specification)
- Oracle: Class Executors (Java API Specification)
- Oracle: Interface Future (Java API Specification)
- dev.java — Concurrency
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
ExecutorServiceDasar.java
Unduh berkasimport java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class ExecutorServiceDasar {
public static void main(String[] args) throws Exception {
ExecutorService eksekutor = Executors.newFixedThreadPool(2);
try {
// Task 1: Callable<String> -- lambda mengembalikan nilai.
Future<String> tugasTeks = eksekutor.submit(
() -> "laporan penjualan siap");
// Task 2: Callable<Integer> -- penjumlahan 1..100 di thread pool.
Future<Integer> tugasHitung = eksekutor.submit(new Callable<Integer>() {
@Override
public Integer call() {
int total = 0;
for (int i = 1; i <= 100; i++) {
total += i;
}
return total;
}
});
// Task 3: Runnable -- aksi tanpa nilai balik; tanda selesai
// dikumpulkan lewat AtomicBoolean, bukan println dari dalam task.
AtomicBoolean arsipBerjalan = new AtomicBoolean(false);
Future<?> tugasAksi = eksekutor.submit(() -> {
arsipBerjalan.set(true); // lakukan pengarsipan...
});
// get() menunggu masing-masing task selesai, lalu hasil dicetak
// oleh main dalam urutan pemasukan -- deterministik.
System.out.println("hasil tugas teks : " + tugasTeks.get());
System.out.println("hasil tugas hitung : " + tugasHitung.get());
tugasAksi.get(); // tunggu Runnable selesai (mengembalikan null)
System.out.println("tugas aksi selesai : " + arsipBerjalan.get());
System.out.println("eksekutor masih hidup: " + !eksekutor.isTerminated());
} finally {
// Matikan eksekutor secara sopan: tolak task baru, tunggu task jalan.
eksekutor.shutdown();
if (!eksekutor.awaitTermination(5, TimeUnit.SECONDS)) {
eksekutor.shutdownNow(); // belum selesai setelah 5 detik: paksa
}
}
// Setelah shutdown, task baru pasti ditolak.
try {
eksekutor.submit(() -> System.out.println("tidak pernah berjalan"));
} catch (RejectedExecutionException e) {
System.out.println("task baru ditolak setelah shutdown");
}
System.out.println("eksekutor dimatikan: " + eksekutor.isTerminated());
}
}
InvokeAllInvokeAny.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class InvokeAllInvokeAny {
public static void main(String[] args) throws Exception {
// Bentuk modern sejak Java 19: try-with-resources -- close() akan
// mematikan eksekutor dan menunggu task berjalan sampai selesai.
try (ExecutorService eksekutor = Executors.newFixedThreadPool(3)) {
// invokeAll: kirim ketiga task, tunggu SEMUANYA selesai.
// Hasil Future terurut sesuai urutan pengiriman, bukan urutan selesai.
List<Callable<Integer>> daftarTugas = List.of(
() -> { Thread.sleep(80); return 10; }, // paling lambat
() -> 20, // tercepat
() -> { Thread.sleep(40); return 30; } // sedang
);
List<Future<Integer>> hasilAll = eksekutor.invokeAll(daftarTugas);
System.out.println("invokeAll -- hasil sesuai urutan pengiriman:");
for (int i = 0; i < hasilAll.size(); i++) {
System.out.println(" task ke-" + (i + 1) + ": " + hasilAll.get(i).get());
}
// invokeAny: kembali dengan hasil task yang PERTAMA berhasil.
// Ketiga task sengaja mengembalikan teks yang sama agar keluaran
// tidak bergantung pada task mana yang memenangkan perlombaan.
List<Callable<String>> pemeriksaan = new ArrayList<>();
for (int sumber = 1; sumber <= 3; sumber++) {
int jeda = sumber * 40; // tiap sumber punya kecepatan berbeda
pemeriksaan.add(() -> {
Thread.sleep(jeda);
return "stok tersedia";
});
}
String hasilAny = eksekutor.invokeAny(pemeriksaan);
System.out.println("invokeAny -- hasil: " + hasilAny);
}
}
}