Ketika satu permintaan pengguna memanggil tiga layanan sekaligus, kode Java klasik cenderung melepas tiga task ke executor lalu berjalan terus — siapa yang menunggu siapa, siapa yang membatalkan siapa, dan di mana hasil dikumpulkan menjadi urusan tiap method sendiri-sendiri. Satu task anak yang gagal sering dibiarkan menggantung sementara saudaranya terus memakan sumber daya. Structured concurrency membalik pola itu dengan satu prinsip: task yang berpecah harus menunggu dalam blok yang sama tempat ia lahir, seperti pemanggilan method yang menyatu kembali ke pemanggilnya. Fitur ini masih preview di Java 25 (JEP 505, preview kelima), sehingga bab ini menampilkan API-nya sebagai kode konsep berpenanda --enable-preview dan menyajikan pola setara yang bisa dijalankan hari ini dengan ExecutorService.
Masalah: Anak Task yang Lepas Kendali #
Concurrency tidak terstruktur muncul dari API lama yang memisahkan pengiriman dan penungguan: executor.submit() mengembalikan Future yang boleh diabaikan, disimpan di mana saja, atau tidak pernah diambil. Akibatnya masa hidup task tidak lagi sejajar dengan masa hidup blok kode yang membuatnya — hubungan induk-anak antar task hilang, observabilitas memburuk (thread dump tidak menunjukkan task mana milik permintaan mana), dan pembatalan harus dirakit manual di tiap titik pemanggilan. Structured concurrency menuntut hubungan itu eksplisit: scope hidup tepat selama blok try, dan semua thread anak berakhir sebelum blok itu selesai.
Wujud Preview di Java 25 #
Bentuk preview saat ini berporos pada StructuredTaskScope yang dibuka lewat open(), diisi lewat fork(), dan ditunggu lewat join(). Kode berikut telah dikompilasi dan dijalankan di JDK 25 dengan flag --enable-preview untuk memastikan bentuknya akurat:
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Subtask;
public class StrukturPreview {
public static void main(String[] args) throws Exception {
try (var scope = StructuredTaskScope.open()) {
Subtask<String> t1 = scope.fork(() -> "hasil-1");
Subtask<Integer> t2 = scope.fork(() -> 42);
scope.join(); // melempar FailedException bila ada subtask gagal
System.out.println(t1.get() + " " + t2.get());
} // close(): menunggu seluruh thread subtask berhenti
}
}java --enable-preview StrukturPreview.javaSaat dijalankan, program mencetak hasil-1 42: kedua subtask berjalan pada virtual thread masing-masing, join() menunggu keduanya, dan close() memastikan tidak ada thread yang lolos keluar blok. Pada uji kedua dengan satu subtask melempar exception, join() melempar StructuredTaskScope.FailedException sementara subtask satunya otomatis dibatalkan — kegagalan menjalar sebagai satu unit, bukan menggantung terpisah. Karena status masih preview, signature dapat berubah pada rilis berikutnya; jangan dipakai di produksi sebelum difinalkan.
Pola yang Sama, Hari Ini, dengan ExecutorService #
Tanpa menunggu preview selesai, tiga aturan intinya bisa ditiru dengan API final hari ini: kumpulkan semua Future, tunggu semuanya, dan batalkan saudara saat satu gagal. Contoh teruji berikut meniru fase join-all dan shutdown-on-failure:
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class PolaTerstruktur {
static String muat(String layanan, int milidetik) throws InterruptedException {
Thread.sleep(milidetik); // penungguan I/O tiruan
return layanan + ": siap";
}
public static void main(String[] args) throws Exception {
try (ExecutorService eksekutor = Executors.newVirtualThreadPerTaskExecutor()) {
// Fase 1 "join-all": induk menunggu seluruh anak tuntas,
// lalu mengumpulkan hasil atau kegagalan masing-masing.
Future<String> katalog = eksekutor.submit(() -> muat("katalog", 40));
Future<String> persediaan = eksekutor.submit(() -> muat("persediaan", 20));
Future<String> galat = eksekutor.submit(() -> {
throw new IllegalStateException("layanan ulasan galat");
});
List<String> laporan = new ArrayList<>();
for (Future<String> f : List.of(katalog, persediaan, galat)) {
try {
laporan.add(f.get());
} catch (ExecutionException e) {
laporan.add("GAGAL: " + e.getCause().getMessage());
}
}
laporan.sort(Comparator.naturalOrder());
laporan.forEach(System.out::println);
// Fase 2 "shutdown-on-failure": anak lambat dibatalkan
// begitu saudaranya gagal cepat.
Future<String> lambat = eksekutor.submit(() -> muat("rekomendasi", 500));
Future<String> cepatGagal = eksekutor.submit(() -> {
throw new IllegalStateException("cek kuota galat");
});
try {
cepatGagal.get();
} catch (ExecutionException e) {
boolean dibatalkan = lambat.cancel(true); // interupsi saudara
System.out.println("pembatalan: " + dibatalkan);
}
} // close(): induk tidak melanjutkan sebelum seluruh anak berhenti
}
}Output yang Dihasilkan:
GAGAL: layanan ulasan galat
katalog: siap
persediaan: siap
pembatalan: trueBedah baris-per-baris untuk bagian pentingnya:
- Blok tiga
submit(...): tiga anak diluncurkan dari satu blok induk; referensiFuture-nya disimpan bersama agar nasibnya jelas satu kelompok. - Loop
for (Future<String> f : List.of(...)): induk memanggilget()untuk tiap anak secara berurutan — penungguan total sama dengan join-all; kegagalan anak ditangkap sebagaiExecutionExceptiondan dikumpulkan, bukan dibiarkan menggantung. - Blok
lambat.cancel(true): begitu anak cepat gagal, induk menginterupsi anak lambat yang masih tidur — peniruan pembatalan menjalar; keluaranpembatalan: truemenegaskan interupsi terkirim. - Baris penutup
}milik try:close()padaExecutorService(Java 19+) menahan induk sampai seluruh anak berhenti — sisi “scope tidak membocorkan thread” dari structured concurrency.
Bagian terakhir dari triad Project Loom yang menunggu giliran finalnya adalah ScopedValue — pengganti ThreadLocal yang sudah difinalkan di Java 25 dan dibahas pada chapter berikutnya.
Referensi #
- OpenJDK: JEP 505 — Structured Concurrency (Fifth Preview)
- Oracle: Interface StructuredTaskScope (Java API Specification, Preview)
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PolaTerstruktur.java
Unduh berkasimport java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class PolaTerstruktur {
static String muat(String layanan, int milidetik) throws InterruptedException {
Thread.sleep(milidetik); // penungguan I/O tiruan
return layanan + ": siap";
}
public static void main(String[] args) throws Exception {
try (ExecutorService eksekutor = Executors.newVirtualThreadPerTaskExecutor()) {
// Fase 1 "join-all": induk menunggu seluruh anak tuntas,
// lalu mengumpulkan hasil atau kegagalan masing-masing.
Future<String> katalog = eksekutor.submit(() -> muat("katalog", 40));
Future<String> persediaan = eksekutor.submit(() -> muat("persediaan", 20));
Future<String> galat = eksekutor.submit(() -> {
throw new IllegalStateException("layanan ulasan galat");
});
List<String> laporan = new ArrayList<>();
for (Future<String> f : List.of(katalog, persediaan, galat)) {
try {
laporan.add(f.get());
} catch (ExecutionException e) {
laporan.add("GAGAL: " + e.getCause().getMessage());
}
}
laporan.sort(Comparator.naturalOrder());
laporan.forEach(System.out::println);
// Fase 2 "shutdown-on-failure": anak lambat dibatalkan
// begitu saudaranya gagal cepat.
Future<String> lambat = eksekutor.submit(() -> muat("rekomendasi", 500));
Future<String> cepatGagal = eksekutor.submit(() -> {
throw new IllegalStateException("cek kuota galat");
});
try {
cepatGagal.get();
} catch (ExecutionException e) {
boolean dibatalkan = lambat.cancel(true); // interupsi saudara
System.out.println("pembatalan: " + dibatalkan);
}
} // close(): induk tidak melanjutkan sebelum seluruh anak berhenti
}
}