Structured Concurrency
Java 25 다섯 번째 프리뷰 StructuredTaskScope의 `open`·`fork`·`join`·`close` 순서를 실행하고 조기 결과 접근 실패, 취소, 문맥 상속을 관찰합니다.
구조화된 동시성은 Java 25의 JEP 505 다섯 번째 프리뷰 API입니다.
StructuredTaskScope는 형제 하위 작업을 하나의 코드 범위에 묶고 소유 작업이 join한 뒤 함께 범위를 벗어나게 합니다.
JDK 24까지의 예제와 달리 Java 25 API는 public 생성자 대신 open() 팩터리와 Joiner를 사용합니다.
컴파일과 실행에는 모두 프리뷰 플래그가 필요합니다.
join 전 Subtask.get 호출
아래 하위 작업은 래치에서 기다리는데 소유자가 join() 전에 결과를 읽습니다.
Subtask.get()은 완료 성공 상태가 아니므로 실제 IllegalStateException이 발생합니다.
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.StructuredTaskScope;
public final class StructuredSubtaskBeforeJoinFailure {
public static void main(String[] args) throws Exception {
CountDownLatch release = new CountDownLatch(1);
try (var scope = StructuredTaskScope.<String>open()) {
StructuredTaskScope.Subtask<String> subtask =
scope.fork(
() -> {
release.await();
return "done";
});
try {
System.out.println(subtask.get());
} finally {
release.countDown();
scope.join();
}
}
}
}실행 순서는 open → fork → join → 성공 결과 읽기 → close입니다.
범위의 소유권과 수명을 벗어나 하위 작업 핸들을 보관하지 않습니다.
javac --enable-preview --release 25 -Xlint:preview StructuredSubtaskBeforeJoinFailure.java
java --enable-preview StructuredSubtaskBeforeJoinFailure
# java.lang.IllegalStateException: Result is unavailable or subtask did not complete successfully독립 조회의 요청 단위 결합
사용자와 학습 세션 조회가 모두 성공해야 응답을 만들 수 있다면 default 범위를 사용합니다.
하나가 실패하면 join()이 실패를 전파하고 나머지 작업을 취소하는 정책을 적용합니다.
import java.time.Duration;
import java.util.concurrent.StructuredTaskScope;
public final class StructuredStudyResponse {
private record User(long id, String name) {}
private record Summary(int sessions, int minutes) {}
private record Response(User user, Summary summary) {}
private static User loadUser() throws InterruptedException {
Thread.sleep(Duration.ofMillis(5));
return new User(7, "min");
}
private static Summary loadSummary() throws InterruptedException {
Thread.sleep(Duration.ofMillis(5));
return new Summary(3, 120);
}
static Response load() throws InterruptedException {
try (var scope = StructuredTaskScope.open()) {
StructuredTaskScope.Subtask<User> user = scope.fork(StructuredStudyResponse::loadUser);
StructuredTaskScope.Subtask<Summary> summary =
scope.fork(StructuredStudyResponse::loadSummary);
scope.join();
return new Response(user.get(), summary.get());
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println(load());
}
}close()는 try-with-resources가 보장합니다.
join() 없이 블록을 나가거나 범위를 잘못 중첩하면 실행 환경에서 구조 위반을 감지할 수 있습니다.
Joiner 완료 정책과 결과 형태
모든 결과가 필요한지, 성공 하나면 되는지, 단순 완료만 기다리는지에 따라 Joiner를 선택합니다.
아래는 두 미러 서버 중 먼저 성공한 값을 결과로 받습니다.
실패한 형제 작업의 처리와 취소는 Joiner 정책이 담당합니다.
import java.time.Duration;
import java.util.concurrent.StructuredTaskScope;
public final class AnySuccessfulMirrorResult {
private static String mirror(String name, long delayMillis, boolean fail) throws Exception {
Thread.sleep(Duration.ofMillis(delayMillis));
if (fail) {
throw new IllegalStateException(name + " unavailable");
}
return name + "-result";
}
public static void main(String[] args) throws InterruptedException {
var joiner = StructuredTaskScope.Joiner.<String>anySuccessfulResultOrThrow();
try (var scope = StructuredTaskScope.open(joiner)) {
scope.fork(() -> mirror("primary", 5, true));
scope.fork(() -> mirror("secondary", 10, false));
String result = scope.join();
System.out.println(result);
}
}
}“먼저 완료”와 “먼저 성공”은 다릅니다. 첫 작업이 실패해도 두 번째 성공을 기다립니다. 두 작업이 모두 외부 부수 효과를 만들면 나머지 작업 취소만으로 이미 발생한 효과를 롤백할 수 없습니다.
구조화된 작업의 ScopedValue
Java 25의 정식 기능인 ScopedValue와 프리뷰 API인 StructuredTaskScope를 결합하면 요청 문맥이 fork된 하위 작업에 상속됩니다.
가변 ThreadLocal 값을 복사하는 방식보다 상위·하위 작업의 코드 범위가 분명합니다.
import java.util.concurrent.StructuredTaskScope;
public final class StructuredScopedContext {
private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
static String gather() throws InterruptedException {
try (var scope = StructuredTaskScope.open()) {
var first = scope.fork(() -> REQUEST_ID.get() + ":user");
var second = scope.fork(() -> REQUEST_ID.get() + ":summary");
scope.join();
return first.get() + " | " + second.get();
}
}
public static void main(String[] args) throws Exception {
String result =
ScopedValue.where(REQUEST_ID, "req-37").call(StructuredScopedContext::gather);
System.out.println(result);
}
}관찰할 실패·취소 항목
- 하위 작업 예외가
join에서 어떤 래퍼와 원인으로 전파되는지 기록합니다. - 형제 작업이 블로킹 호출에서
interrupt에 반응하는지 확인합니다. - 상위 작업
interrupt가 범위와 하위 작업에 어떻게 전달되는지 봅니다. - 시간 제한 구성이 기한 초과 작업을 취소하는지 측정합니다.
- JFR·스레드 덤프에서 상위 작업과 하위 작업 관계가 어떻게 보이는지 관찰합니다.
프리뷰 API의 시그니처는 다음 릴리스에서 달라질 수 있으므로 래퍼 인터페이스 뒤에 실험 구현으로 둡니다.
운영 서비스의 public 시그니처에 StructuredTaskScope 타입을 직접 노출하지 않습니다.
StructuredTaskScope가 ExecutorService를 대체하나요?
아닙니다.
JEP의 목표도 모든 동시성 API를 대체하는 것이 아닙니다.
상위 요청의 코드 범위 안에서 관련 하위 작업을 병렬로 분기하고 join하는 문제에 적합합니다.
장기 백그라운드 큐, 예약 실행, 독립 작업 제출에는 기존 실행기나 메시징 시스템이 더 적합합니다.
연습 문제
기본 범위에서 집중 점수와 복습 점수를 fork하고 join 뒤 합계를 반환하세요.
하나가 실패하면 합계를 만들지 않아야 합니다.
해설 보기
import java.util.concurrent.StructuredTaskScope;
public final class StructuredScoreSolution {
private static int focusScore() {
return 70;
}
private static int reviewScore() {
return 25;
}
static int total() throws InterruptedException {
try (var scope = StructuredTaskScope.open()) {
var focus = scope.fork(StructuredScoreSolution::focusScore);
var review = scope.fork(StructuredScoreSolution::reviewScore);
scope.join();
return focus.get() + review.get();
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("total=" + total());
}
}실행 명령은 컴파일·실행 모두 프리뷰 플래그를 포함하며 결과는 total=95입니다.