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:

text
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: true

Mari kita bedah alurnya:

  1. 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.
  2. Tiga bentuk submit(): lambda Callable (baris () -> "laporan..."), class anonim Callable<Integer> (bentuk eksplisit), dan lambda Runnable (Future<?>). Ketiganya masuk antrean yang sama.
  3. Tidak ada println dari dalam task: semua hasil dikumpulkan lewat nilai balik atau AtomicBoolean, lalu main mencetak setelah get() kembali. Dengan begitu urutan keluaran ditentukan oleh urutan pemanggilan get(), bukan oleh kecepatan thread.
  4. eksekutor masih hidup: true: isTerminated() masih false karena eksekutor belum dimatikan — semua task selesai, tetapi pool masih siap menerima task baru.
  5. Blok finally dengan shutdown(): 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:

text
invokeAll -- hasil sesuai urutan pengiriman:
  task ke-1: 10
  task ke-2: 20
  task ke-3: 30
invokeAny -- hasil: stok tersedia

Tiga perilaku yang perlu diingat dari contoh tersebut:

  1. Urutan invokeAll tetap 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.
  2. invokeAny perlombaan 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.
  3. Thread.sleep di dalam Callable: meniru pekerjaan nyata (membaca berkas, memanggil layanan) yang memakan waktu bervariasi. Interupsi tetap terpropagasi lewat InterruptedException, sehingga shutdownNow() 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

ExecutorServiceDasar.java

Unduh berkas
java
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());
    }
}

InvokeAllInvokeAny.java

Unduh berkas
java
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);
        }
    }
}