Menjalankan beberapa subtask sekaligus mudah; menjaga mereka tetap satu tim jauh lebih sulit. Ketika satu subtask gagal, subtask lain yang masih berjalan biasanya dibiarkan menggantung tanpa hasil — fenomena thread leak yang memakan sumber daya tanpa manfaat. Structured concurrency mengajawab masalah ini dengan prinsip sederhana: task yang berhubungan harus hidup dan berakhir dalam satu unit kerja, seperti blok kode memiliki kurung pembuka dan penutup. Pada Chapter ini kita akan melihat masalah yang diselesaikannya, pola manual yang mendekatinya, dan API StructuredTaskScope yang dirancang khusus untuk pola ini.

Masalah: Subtask yang Menggantung #

Bayangkan sebuah layanan agregator yang mengambil data dari tiga sumber paralel. Sumber kedua gagal seketika, sumber ketiga lambat dua detik. Pola ExecutorService klasik dengan invokeAll akan menunggu semua task selesai — termasuk yang hasilnya sudah tidak dibutuhkan karena satu subtask gagal. Kode bisnis akhirnya menunggu dua detik penuh demi data yang akan dibuang, dan pemanggil tidak mendapat kegagalan lebih cepat. Perilaku “shutdown on failure” — berhenti seketika di kegagalan pertama dan membatalkan sisanya — harus ditulis tangan.

Pola Fail-Fast Manual #

Sebelum API khusus ada, kita bisa menyusun pola fail-fast sendiri dengan ExecutorService dan Future. Program berikut mengirim tiga task, mengawasi kegagalan pertama, lalu membatalkan task yang tersisa:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class FailFastManual {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService eksekutor = Executors.newFixedThreadPool(3);
        List<Callable<Integer>> sumberData = List.of(
                () -> {                       // sumber-1: cepat dan sehat
                    Thread.sleep(30);
                    return 10;
                },
                () -> {                       // sumber-2: gagal seketika
                    throw new IllegalStateException("sumber-2 tidak merespons");
                },
                () -> {                       // sumber-3: lambat tapi sehat
                    Thread.sleep(2_000);
                    return 30;
                });

        // Pola fail-fast manual: kirim semuanya, berhenti di kegagalan
        // pertama, batalkan sisanya -- inti semantik "shutdown on failure".
        List<Future<Integer>> futures = new ArrayList<>();
        for (Callable<Integer> tugas : sumberData) {
            futures.add(eksekutor.submit(tugas));
        }

        List<Integer> hasil = new ArrayList<>();
        String kegagalan = null;
        try {
            for (Future<Integer> f : futures) {
                hasil.add(f.get()); // berhenti pada task pertama yang gagal
            }
        } catch (ExecutionException e) {
            for (Future<Integer> f : futures) {
                f.cancel(true); // interupsi task yang masih berjalan
            }
            kegagalan = e.getCause().getMessage();
        } finally {
            eksekutor.shutdown();
        }

        System.out.println("hasil terkumpul  : " + hasil);
        System.out.println("task yang gagal  : " + kegagalan);
        System.out.println("task-3 dibatalkan: " + futures.get(2).isCancelled());
    }
}

Output yang Dihasilkan:

text
hasil terkumpul  : [10]
task yang gagal  : sumber-2 tidak merespons
task-3 dibatalkan: true

Mari kita telaah logika inti program tersebut:

  1. Tiga Callable dengan karakter berbeda: sumber pertama cepat, sumber kedua gagal seketika dengan exception, dan sumber ketiga sehat tetapi lambat dua detik.
  2. Pengiriman bertahap lalu pengawasan: seluruh task dikirim, kemudian program memeriksa status satu per satu. Begitu kegagalan pertama terbaca, task ketiga dibatalkan lewat Future.cancel(true) sehingga Thread.sleep di dalamnya terinterupsi dan berakhir jauh lebih cepat daripada dua detik.
  3. Hasil terkumpul hanya dari task sehat: nilai 10 dari sumber pertama tetap sah untuk dicatat, sementara kegagalan sumber kedua dan pembatalan sumber ketiga dilaporkan terpisah.

Pola manual seperti ini bekerja, tetapi setiap aplikasi harus menulis ulang logika yang sama: kirim, awasi, batalkan, laporkan. Kerumitan tersebut tersebar dan rawan keliru — tepat seperti pengelolaan memori manual sebelum garbage collector.

API StructuredTaskScope #

Java mengemas pola ini menjadi API di paket java.util.concurrent melalui StructuredTaskScope. Bentuk penggunaannya terlihat seperti ini (masih berstatus preview di Java 25, sehingga perlu flag --enable-preview untuk menjalankannya):

java
Response hasil;
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Subtask<Sumber1> a = scope.fork(() -> ambilSumber1());
    Subtask<Sumber2> b = scope.fork(() -> ambilSumber2());
    Subtask<Sumber3> c = scope.fork(() -> ambilSumber3());

    scope.join();           // menunggu semua subtask selesai
    scope.throwIfFailed();  // melempar bila ada yang gagal

    hasil = gabungkan(a.get(), b.get(), c.get());
}

Bedah singkatnya: method fork menjalankan subtask sebagai anak dari scope saat ini; join menjamin tidak ada subtask yang lolos keluar blok; ShutdownOnFailure membatalkan seluruh subtask lain begitu satu di antaranya gagal; dan throwIfFailed meneruskan kegagalan ke pemanggil sebagai satu exception utuh. Seluruh siklus hidup subtask dibatasi kurung blok try — persis struktur blok yang membuat kode mudah dianalisis. Varian lain seperti ShutdownOnSuccess tersedia untuk kebutuhan “ambil hasil tercepat”.

Keterikatan dan Pohon Eksekusi #

Structured concurrency membawa konsep task tree: setiap scope memiliki pemilik, dan subtask selalu berakhir sebelum pemiliknya melanjutkan. Struktur pohon ini membuat diagnosis jauh lebih mudah — thread dump memperlihatkan relasi induk-anak, kegagalan tidak pernah “bocor” keluar scope, dan konteks seperti ThreadLocal atau ScopedValue terwariskan secara terprediksi. Kombinasi ScopedValue (pembagian data immutable lintas subtask) dan structured concurrency menjadi pasangan yang dirancang saling melengkapi untuk kode server modern.

Prinsip yang perlu dibawa: jika beberapa task membentuk satu unit kerja logis, jarkan mereka dalam satu scope yang menjamin hidup dan mati bersama. Chapter berikutnya merangkum seluruh ancaman thread safety — visibility, atomicity, dan publication — menjadi satu panduan praktis menjaga kebenaran kode paralel.

Referensi #

Berkas Sumber

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

FailFastManual.java

Unduh berkas
java
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class FailFastManual {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService eksekutor = Executors.newFixedThreadPool(3);
        List<Callable<Integer>> sumberData = List.of(
                () -> {                       // sumber-1: cepat dan sehat
                    Thread.sleep(30);
                    return 10;
                },
                () -> {                       // sumber-2: gagal seketika
                    throw new IllegalStateException("sumber-2 tidak merespons");
                },
                () -> {                       // sumber-3: lambat tapi sehat
                    Thread.sleep(2_000);
                    return 30;
                });

        // Pola fail-fast manual: kirim semuanya, berhenti di kegagalan
        // pertama, batalkan sisanya -- inti semantik "shutdown on failure".
        List<Future<Integer>> futures = new ArrayList<>();
        for (Callable<Integer> tugas : sumberData) {
            futures.add(eksekutor.submit(tugas));
        }

        List<Integer> hasil = new ArrayList<>();
        String kegagalan = null;
        try {
            for (Future<Integer> f : futures) {
                hasil.add(f.get()); // berhenti pada task pertama yang gagal
            }
        } catch (ExecutionException e) {
            for (Future<Integer> f : futures) {
                f.cancel(true); // interupsi task yang masih berjalan
            }
            kegagalan = e.getCause().getMessage();
        } finally {
            eksekutor.shutdown();
        }

        System.out.println("hasil terkumpul  : " + hasil);
        System.out.println("task yang gagal  : " + kegagalan);
        System.out.println("task-3 dibatalkan: " + futures.get(2).isCancelled());
    }
}