Virtual threads help waiting work (HTTP, database, files). For computing work use a
pool sized to your CPU cores.
Executors and futures
try(varexecutor=Executors.newVirtualThreadPerTaskExecutor()){Future<Integer>future=executor.submit(()->expensive());varresult=future.get();// blocks until ready}vartasks=List.<Callable<String>>of(()->fetch("https://example.test/a"),()->fetch("https://example.test/b"));try(varexecutor=Executors.newVirtualThreadPerTaskExecutor()){for(varf:executor.invokeAll(tasks))IO.println(f.get());}
Structured concurrency (preview in 25)
Subtasks that belong together should live and die together: if one fails, the others are
cancelled.
// run with: java --enable-preview --source 25 File.javaimportjava.util.concurrent.StructuredTaskScope;recordProfile(Stringdetails,Stringorders){}ProfileloadProfile(Stringid)throwsException{try(varscope=StructuredTaskScope.open()){vardetails=scope.fork(()->loadDetails(id));varorders=scope.fork(()->loadOrders(id));scope.join();// waits for both, cancels on failurereturnnewProfile(details.get(),orders.get());}}
No orphaned threads, no forgotten cleanup, readable stack traces.
Scoped values (Java 25)
ScopedValue replaces ThreadLocal for passing context (user, request id, tenant) down the
call stack. Immutable and cleaned up automatically.
finalstaticScopedValue<String>USER=ScopedValue.newInstance();voidhandleRequest(Stringuser){ScopedValue.where(USER,user).run(()->{checkPermissions();writeAuditLog();// sees USER without a parameter});}voidwriteAuditLog(){IO.println("action by "+USER.get());}
Shared state is the hard part
varcounter=newint[1];try(vare=Executors.newVirtualThreadPerTaskExecutor()){for(vari=0;i<1000;i++)e.submit(()->counter[0]++);// broken: not atomic}IO.println(counter[0]);//almostnever1000
Immutable objects (records!) are thread-safe by construction. Less shared mutable state means
less synchronisation — and fewer bugs that only show up in production.