Series navigation
Written by
Jagdish Salgotra
Software engineer with 15 years work experience. Skills: Java, Spring Boot, Hibernate, SQL, Linux, Python, Telecom, IoT, Autonomous Systems
to join the discussion
Master resource-aware scheduling and bulkhead isolation in Java structured concurrency. Learn CPU/memory-aware throttling, adaptive concurrency, and workload isolation patterns using StructuredTaskScope and virtual threads.
Written by
Software engineer with 15 years work experience. Skills: Java, Spring Boot, Hibernate, SQL, Linux, Python, Telecom, IoT, Autonomous Systems
Note This article uses Java 21 preview
StructuredTaskScopeAPIs (JEP 453). API changes in later previews are covered in Part 9. Compile and run with--enable-preview.
Virtual threads and structured scopes improve concurrency ergonomics, but they do not remove hard limits:
Without explicit resource policy, request fan-out can overload dependencies.
Use semaphores (or equivalent admission controls) to match real downstream capacity.
public List<String> executeResourceAware(List<ResourceTask> tasks) throws Exception {
var cpuTasks = tasks.stream().filter(t -> t.getType() == ResourceType.CPU).toList();
var memoryTasks = tasks.stream().filter(t -> t.getType() == ResourceType.MEMORY).toList();
var ioTasks = tasks.stream().filter(t -> t.getType() == ResourceType.IO).toList();
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var cpuResult = scope.fork(() -> executeResourceGroup(cpuTasks));
var memoryResult = scope.fork(() -> executeResourceGroup(memoryTasks));
var ioResult = scope.fork(() -> executeResourceGroup(ioTasks));
scope.join();
scope.throwIfFailed();
List<String> allResults = new ArrayList<>();
allResults.addAll(cpuResult.get());
allResults.addAll(memoryResult.get());
allResults.addAll(ioResult.get());
return allResults;
}
}
Then use these guards inside scope subtasks.
private List<String> executeResourceGroup(List<ResourceTask> tasks) throws Exception {
List<String> results = new ArrayList<>();
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
List<StructuredTaskScope.Subtask<String>> subtasks = new ArrayList<>();
for (ResourceTask task : tasks) {
subtasks.add(scope.fork(() -> {
Thread.sleep(task.getDuration() * 100);
return task.getName() + " completed";
}));
}
scope.join();
scope.throwIfFailed();
for (var subtask : subtasks) {
results.add(subtask.get());
}
}
return results;
}
This keeps orchestration readable while enforcing dependency capacity boundaries.
For CPU-heavy transforms, use bounded pools explicitly.
public List<T> executeAdaptive(List<Callable<T>> tasks) throws Exception {
int batchSize = Math.min(5, tasks.size());
List<T> allResults = new ArrayList<>();
for (int i = 0; i < tasks.size(); i += batchSize) {
int end = Math.min(i + batchSize, tasks.size());
List<Callable<T>> batch = tasks.subList(i, end);
long startTime = System.currentTimeMillis();
List<T> batchResults = executeBatch(batch);
long duration = System.currentTimeMillis() - startTime;
allResults.addAll(batchResults);
if (duration < 100) {
batchSize = Math.min(batchSize * 2, 10);
} else if (duration > 500) {
batchSize = Math.max(batchSize / 2, 2);
}
}
return allResults;
}
Structured concurrency is most valuable for lifecycle and I/O orchestration; CPU saturation still requires bounded execution design.
Reject or degrade requests early when resource pressure is high.
public String bulkheadPattern() throws Exception {
try (var criticalScope = new StructuredTaskScope.ShutdownOnFailure();
var nonCriticalScope = new StructuredTaskScope.ShutdownOnFailure()) {
var criticalService1 = criticalScope.fork(() -> simulateServiceCall("critical-auth", 100));
var criticalService2 = criticalScope.fork(() -> simulateServiceCall("critical-payment", 150));
var nonCriticalService1 = nonCriticalScope.fork(() -> simulateServiceCall("analytics", 200));
var nonCriticalService2 = nonCriticalScope.fork(() -> simulateServiceCall("logging", 50));
criticalScope.join();
criticalScope.throwIfFailed();
try {
nonCriticalScope.join();
nonCriticalScope.throwIfFailed();
} catch (Exception e) {
logger.warn("Non-critical services failed: {}", e.getMessage());
}
String result = String.format("Bulkhead Pattern: Critical[%s, %s] Non-Critical[%s, %s]",
criticalService1.get(), criticalService2.get(),
"analytics-ok", "logging-ok");
return result;
}
}
Early admission control often protects p99 better than deep queueing.
throwIfFailed() consistently in ShutdownOnFailure scopes.For resource-aware scheduling, test:
javac --release 21 --enable-preview ...
java --enable-preview ...