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:
hasil terkumpul : [10]
task yang gagal : sumber-2 tidak merespons
task-3 dibatalkan: trueMari kita telaah logika inti program tersebut:
- Tiga
Callabledengan karakter berbeda: sumber pertama cepat, sumber kedua gagal seketika dengan exception, dan sumber ketiga sehat tetapi lambat dua detik. - 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)sehinggaThread.sleepdi dalamnya terinterupsi dan berakhir jauh lebih cepat daripada dua detik. - Hasil terkumpul hanya dari task sehat: nilai
10dari 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):
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 #
- JEP 505: Structured Concurrency (Fifth Preview)
- JEP 444: Virtual Threads
- java.util.concurrent package summary
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
FailFastManual.java
Unduh berkasimport 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());
}
}