Overview
Scoped Values are a modern alternative to Thread-Local variables introduced in Java 20 (as a preview feature) and further refined in later versions. They provide a safer and more efficient way to share immutable data within and across threads.
Key Differences from Thread-Local
| Aspect | Thread-Local | Scoped Values |
|---|---|---|
| Mutability | Mutable | Immutable |
| Inheritance | Manual (InheritableThreadLocal) | Automatic |
| Memory | Can cause memory leaks | Automatic cleanup |
| Performance | Slower access | Faster access |
| Scope | Thread lifetime | Bounded lifetime |
Basic Usage
1. Creating and Using Scoped Values
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Subtask;
public class ScopedValuesBasic {
// 1. Define a ScopedValue
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
private static final ScopedValue<Locale> USER_LOCALE = ScopedValue.newInstance();
private static final ScopedValue<RequestContext> REQUEST_CONTEXT = ScopedValue.newInstance();
public static void main(String[] args) {
// 2. Bind values and run code within that scope
User user = new User("john_doe", "John Doe", "en-US");
RequestContext context = new RequestContext("req-123", System.currentTimeMillis());
ScopedValue.where(CURRENT_USER, user)
.where(REQUEST_CONTEXT, context)
.run(() -> processRequest());
// Outside the scope, ScopedValue is unbound
System.out.println("Outside scope: " +
(CURRENT_USER.isBound() ? "Bound" : "Unbound")); // Unbound
}
private static void processRequest() {
// 3. Access the bound value within the scope
User user = CURRENT_USER.get();
RequestContext context = REQUEST_CONTEXT.get();
System.out.println("Processing request for: " + user.username());
System.out.println("Request ID: " + context.requestId());
// Call methods that also need the context
performBusinessOperation();
}
private static void performBusinessOperation() {
// Scoped values are automatically available in called methods
User user = CURRENT_USER.get();
System.out.println("Business operation for: " + user.username());
}
// Record definitions
record User(String username, String fullName, String locale) {}
record RequestContext(String requestId, long timestamp) {}
}
2. Nested Scopes and Rebinding
public class ScopedValuesNested {
private static final ScopedValue<String> CURRENT_TENANT = ScopedValue.newInstance();
private static final ScopedValue<Integer> DEPTH = ScopedValue.newInstance();
public static void main(String[] args) {
// Outer scope
ScopedValue.where(CURRENT_TENANT, "acme-corp")
.where(DEPTH, 1)
.run(() -> {
System.out.println("Outer - Tenant: " + CURRENT_TENANT.get());
System.out.println("Outer - Depth: " + DEPTH.get());
// Nested scope - can rebind values
ScopedValue.where(CURRENT_TENANT, "another-tenant")
.where(DEPTH, 2)
.run(() -> {
System.out.println("Inner - Tenant: " + CURRENT_TENANT.get());
System.out.println("Inner - Depth: " + DEPTH.get());
});
// Back to outer scope values
System.out.println("Back to Outer - Tenant: " + CURRENT_TENANT.get());
System.out.println("Back to Outer - Depth: " + DEPTH.get());
});
}
}
Practical Examples
Example 1: Web Request Context Management
public class WebRequestScopedValues {
// Scoped values for request context
private static final ScopedValue<RequestContext> REQUEST = ScopedValue.newInstance();
private static final ScopedValue<UserPrincipal> USER = ScopedValue.newInstance();
private static final ScopedValue<Locale> LOCALE = ScopedValue.newInstance();
private static final ScopedValue<Map<String, String>> TRACING_HEADERS = ScopedValue.newInstance();
public static class RequestContext {
private final String requestId;
private final String sessionId;
private final String userAgent;
private final Instant startTime;
public RequestContext(String requestId, String sessionId, String userAgent) {
this.requestId = requestId;
this.sessionId = sessionId;
this.userAgent = userAgent;
this.startTime = Instant.now();
}
// Getters
public String getRequestId() { return requestId; }
public String getSessionId() { return sessionId; }
public String getUserAgent() { return userAgent; }
public Instant getStartTime() { return startTime; }
}
public static class UserPrincipal {
private final String userId;
private final String username;
private final Set<String> roles;
public UserPrincipal(String userId, String username, Set<String> roles) {
this.userId = userId;
this.username = username;
this.roles = Set.copyOf(roles);
}
// Getters
public String getUserId() { return userId; }
public String getUsername() { return username; }
public Set<String> getRoles() { return roles; }
public boolean hasRole(String role) {
return roles.contains(role);
}
}
// Simulated HTTP request handler
public static void handleHttpRequest(HttpServletRequest request, HttpServletResponse response) {
// Extract context from request
RequestContext context = extractRequestContext(request);
UserPrincipal user = authenticateUser(request);
Locale locale = extractLocale(request);
Map<String, String> tracingHeaders = extractTracingHeaders(request);
// Bind scoped values and process request
ScopedValue.where(REQUEST, context)
.where(USER, user)
.where(LOCALE, locale)
.where(TRACING_HEADERS, tracingHeaders)
.run(() -> processRequest(request, response));
}
private static void processRequest(HttpServletRequest request, HttpServletResponse response) {
try {
// Access scoped values anywhere in the call stack
RequestContext context = REQUEST.get();
UserPrincipal user = USER.get();
logRequestStart(context, user);
// Route to appropriate handler
String path = request.getPathInfo();
if (path.startsWith("/api/users")) {
handleUserApi(request, response);
} else if (path.startsWith("/api/products")) {
handleProductApi(request, response);
} else {
handleDefault(request, response);
}
logRequestEnd(context);
} catch (Exception e) {
handleError(e, response);
}
}
private static void handleUserApi(HttpServletRequest request, HttpServletResponse response) {
UserPrincipal currentUser = USER.get();
RequestContext context = REQUEST.get();
// Check permissions
if (!currentUser.hasRole("USER_READ")) {
throw new SecurityException("Insufficient permissions");
}
// Business logic that needs user context
UserService userService = new UserService();
userService.getUserProfile(currentUser.getUserId());
response.setStatus(200);
}
private static void handleProductApi(HttpServletRequest request, HttpServletResponse response) {
// Access tracing headers for distributed tracing
Map<String, String> tracing = TRACING_HEADERS.get();
String traceId = tracing.get("trace-id");
// Log with trace context
System.out.println("Processing product API with trace: " + traceId);
ProductService productService = new ProductService();
productService.listProducts();
response.setStatus(200);
}
// Utility methods
private static RequestContext extractRequestContext(HttpServletRequest request) {
return new RequestContext(
generateRequestId(),
request.getSession().getId(),
request.getHeader("User-Agent")
);
}
private static UserPrincipal authenticateUser(HttpServletRequest request) {
// Simulated authentication
String authHeader = request.getHeader("Authorization");
// ... authentication logic
return new UserPrincipal("user-123", "john_doe", Set.of("USER_READ", "PRODUCT_READ"));
}
private static Locale extractLocale(HttpServletRequest request) {
String langHeader = request.getHeader("Accept-Language");
return langHeader != null ? Locale.forLanguageTag(langHeader) : Locale.getDefault();
}
private static Map<String, String> extractTracingHeaders(HttpServletRequest request) {
Map<String, String> tracing = new HashMap<>();
tracing.put("trace-id", request.getHeader("X-Trace-Id"));
tracing.put("span-id", request.getHeader("X-Span-Id"));
return Map.copyOf(tracing);
}
private static void logRequestStart(RequestContext context, UserPrincipal user) {
System.out.printf("[%s] Request started for user %s%n",
context.getRequestId(), user.getUsername());
}
private static void logRequestEnd(RequestContext context) {
Duration duration = Duration.between(context.getStartTime(), Instant.now());
System.out.printf("[%s] Request completed in %d ms%n",
context.getRequestId(), duration.toMillis());
}
private static void handleError(Exception e, HttpServletResponse response) {
response.setStatus(500);
// Log error with request context
RequestContext context = REQUEST.get();
System.err.printf("[%s] Request failed: %s%n", context.getRequestId(), e.getMessage());
}
private static String generateRequestId() {
return "req-" + System.currentTimeMillis() + "-" + Math.abs(new Random().nextInt());
}
// Service classes that use scoped values
public static class UserService {
public UserProfile getUserProfile(String userId) {
// Access scoped values in service layer
UserPrincipal currentUser = USER.get();
RequestContext context = REQUEST.get();
// Authorization check
if (!currentUser.getUserId().equals(userId) && !currentUser.hasRole("ADMIN")) {
throw new SecurityException("Cannot access other user's profile");
}
// Business logic with context
System.out.printf("[%s] Fetching profile for user %s%n",
context.getRequestId(), userId);
return new UserProfile(userId, currentUser.getUsername());
}
}
public static class ProductService {
public List<Product> listProducts() {
Locale locale = LOCALE.get();
Map<String, String> tracing = TRACING_HEADERS.get();
// Use locale for internationalization
System.out.println("Listing products for locale: " + locale);
System.out.println("Trace ID: " + tracing.get("trace-id"));
return List.of(new Product("prod-1", "Product 1"), new Product("prod-2", "Product 2"));
}
}
record UserProfile(String userId, String username) {}
record Product(String id, String name) {}
}
Example 2: Structured Concurrency with Scoped Values
public class StructuredConcurrencyScopedValues {
private static final ScopedValue<Project> CURRENT_PROJECT = ScopedValue.newInstance();
private static final ScopedValue<String> CORRELATION_ID = ScopedValue.newInstance();
public static class Project {
private final String id;
private final String name;
private final Set<String> permissions;
public Project(String id, String name, Set<String> permissions) {
this.id = id;
this.name = name;
this.permissions = Set.copyOf(permissions);
}
// Getters
public String getId() { return id; }
public String getName() { return name; }
public Set<String> getPermissions() { return permissions; }
}
public static class ProjectService {
public ProjectSummary getProjectSummary(String projectId) throws Exception {
Project project = loadProject(projectId);
String correlationId = generateCorrelationId();
return ScopedValue.where(CURRENT_PROJECT, project)
.where(CORRELATION_ID, correlationId)
.call(() -> fetchProjectSummary());
}
private ProjectSummary fetchProjectSummary() throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// Launch concurrent subtasks - they inherit scoped values automatically
Subtask<List<TeamMember>> membersTask = scope.fork(this::fetchTeamMembers);
Subtask<ProjectStatistics> statsTask = scope.fork(this::fetchProjectStatistics);
Subtask<List<ProjectTask>> tasksTask = scope.fork(this::fetchRecentTasks);
// Wait for all subtasks to complete
scope.join();
scope.throwIfFailed();
// Combine results
return new ProjectSummary(
CURRENT_PROJECT.get(),
membersTask.get(),
statsTask.get(),
tasksTask.get()
);
}
}
private List<TeamMember> fetchTeamMembers() {
Project project = CURRENT_PROJECT.get();
String correlationId = CORRELATION_ID.get();
System.out.printf("[%s] Fetching team members for project %s%n",
correlationId, project.getId());
// Simulate API call
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return List.of(
new TeamMember("user1", "Alice", "Developer"),
new TeamMember("user2", "Bob", "Designer")
);
}
private ProjectStatistics fetchProjectStatistics() {
Project project = CURRENT_PROJECT.get();
String correlationId = CORRELATION_ID.get();
System.out.printf("[%s] Fetching statistics for project %s%n",
correlationId, project.getId());
// Simulate database query
try { Thread.sleep(150); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return new ProjectStatistics(85, 42, 1500000);
}
private List<ProjectTask> fetchRecentTasks() {
Project project = CURRENT_PROJECT.get();
String correlationId = CORRELATION_ID.get();
System.out.printf("[%s] Fetching recent tasks for project %s%n",
correlationId, project.getId());
// Simulate external service call
try { Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return List.of(
new ProjectTask("task1", "Implement feature", "IN_PROGRESS"),
new ProjectTask("task2", "Write tests", "TODO")
);
}
private Project loadProject(String projectId) {
// Simulate project loading
return new Project(projectId, "Sample Project", Set.of("READ", "WRITE"));
}
private String generateCorrelationId() {
return "corr-" + System.currentTimeMillis();
}
}
// Record definitions
record ProjectSummary(Project project, List<TeamMember> teamMembers,
ProjectStatistics statistics, List<ProjectTask> recentTasks) {}
record TeamMember(String id, String name, String role) {}
record ProjectStatistics(int completionPercentage, int openIssues, long budget) {}
record ProjectTask(String id, String description, String status) {}
public static void main(String[] args) throws Exception {
ProjectService service = new ProjectService();
ProjectSummary summary = service.getProjectSummary("proj-123");
System.out.println("Project: " + summary.project().getName());
System.out.println("Team members: " + summary.teamMembers().size());
System.out.println("Completion: " + summary.statistics().completionPercentage() + "%");
}
}
Example 3: Database Transaction Management
public class TransactionScopedValues {
private static final ScopedValue<Connection> DB_CONNECTION = ScopedValue.newInstance();
private static final ScopedValue<TransactionContext> TX_CONTEXT = ScopedValue.newInstance();
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
public static class TransactionContext {
private final String transactionId;
private final IsolationLevel isolationLevel;
private final boolean readOnly;
private final Instant startTime;
public TransactionContext(String transactionId, IsolationLevel isolationLevel, boolean readOnly) {
this.transactionId = transactionId;
this.isolationLevel = isolationLevel;
this.readOnly = readOnly;
this.startTime = Instant.now();
}
// Getters
public String getTransactionId() { return transactionId; }
public IsolationLevel getIsolationLevel() { return isolationLevel; }
public boolean isReadOnly() { return readOnly; }
public Instant getStartTime() { return startTime; }
}
public enum IsolationLevel {
READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
}
public static class TransactionalService {
public <T> T executeInTransaction(TransactionCallback<T> callback) {
return executeInTransaction(IsolationLevel.READ_COMMITTED, false, callback);
}
public <T> T executeInTransaction(IsolationLevel isolationLevel,
boolean readOnly,
TransactionCallback<T> callback) {
String txId = generateTransactionId();
TransactionContext txContext = new TransactionContext(txId, isolationLevel, readOnly);
try (Connection connection = DataSource.getConnection()) {
// Set up transaction
connection.setAutoCommit(false);
connection.setTransactionIsolation(isolationLevel.ordinal() + 1);
return ScopedValue.where(DB_CONNECTION, connection)
.where(TX_CONTEXT, txContext)
.where(CURRENT_USER, getCurrentUser())
.call(() -> {
try {
T result = callback.doInTransaction();
connection.commit();
logTransactionSuccess(txContext);
return result;
} catch (Exception e) {
connection.rollback();
logTransactionFailure(txContext, e);
throw new TransactionException("Transaction failed", e);
}
});
} catch (SQLException e) {
throw new TransactionException("Failed to manage transaction", e);
}
}
public void updateUserProfile(String userId, UserProfile updates) {
executeInTransaction(() -> {
// All database operations in this scope use the same connection
UserDAO userDao = new UserDAO();
AuditDAO auditDao = new AuditDAO();
User user = userDao.findById(userId);
if (user == null) {
throw new IllegalArgumentException("User not found: " + userId);
}
// Update user
userDao.update(userId, updates);
// Audit the change
auditDao.logUserUpdate(userId, updates, CURRENT_USER.get());
return null;
});
}
public Order processOrder(OrderRequest request) {
return executeInTransaction(IsolationLevel.REPEATABLE_READ, false, () -> {
OrderDAO orderDao = new OrderDAO();
InventoryDAO inventoryDao = new InventoryDAO();
PaymentDAO paymentDao = new PaymentDAO();
// Check inventory
if (!inventoryDao.hasSufficientStock(request.productId(), request.quantity())) {
throw new InsufficientStockException("Not enough stock");
}
// Process payment
PaymentResult payment = paymentDao.processPayment(
request.paymentMethod(), request.amount());
if (!payment.isSuccess()) {
throw new PaymentException("Payment failed: " + payment.getErrorMessage());
}
// Create order
Order order = orderDao.createOrder(request, CURRENT_USER.get());
// Update inventory
inventoryDao.reduceStock(request.productId(), request.quantity());
// Log audit trail
auditOrderCreation(order);
return order;
});
}
private void auditOrderCreation(Order order) {
TransactionContext txContext = TX_CONTEXT.get();
User currentUser = CURRENT_USER.get();
AuditDAO auditDao = new AuditDAO();
auditDao.logOrderCreation(order, currentUser, txContext.getTransactionId());
}
private String generateTransactionId() {
return "tx-" + System.currentTimeMillis() + "-" + Math.abs(new Random().nextInt(1000));
}
private User getCurrentUser() {
// In real application, get from security context
return new User("current-user", "System User");
}
private void logTransactionSuccess(TransactionContext context) {
Duration duration = Duration.between(context.getStartTime(), Instant.now());
System.out.printf("[%s] Transaction completed successfully in %d ms%n",
context.getTransactionId(), duration.toMillis());
}
private void logTransactionFailure(TransactionContext context, Exception e) {
System.err.printf("[%s] Transaction failed: %s%n",
context.getTransactionId(), e.getMessage());
}
}
// DAO classes that use scoped connection
public static class UserDAO {
public User findById(String userId) {
Connection connection = DB_CONNECTION.get();
TransactionContext txContext = TX_CONTEXT.get();
try (PreparedStatement stmt = connection.prepareStatement(
"SELECT * FROM users WHERE id = ?")) {
stmt.setString(1, userId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return new User(rs.getString("id"), rs.getString("name"));
}
return null;
} catch (SQLException e) {
throw new DataAccessException("Failed to find user", e);
}
}
public void update(String userId, UserProfile updates) {
Connection connection = DB_CONNECTION.get();
try (PreparedStatement stmt = connection.prepareStatement(
"UPDATE users SET name = ?, email = ? WHERE id = ?")) {
stmt.setString(1, updates.name());
stmt.setString(2, updates.email());
stmt.setString(3, userId);
stmt.executeUpdate();
} catch (SQLException e) {
throw new DataAccessException("Failed to update user", e);
}
}
}
public static class AuditDAO {
public void logUserUpdate(String userId, UserProfile updates, User currentUser) {
Connection connection = DB_CONNECTION.get();
TransactionContext txContext = TX_CONTEXT.get();
try (PreparedStatement stmt = connection.prepareStatement(
"INSERT INTO audit_log (action, user_id, target_user, transaction_id) VALUES (?, ?, ?, ?)")) {
stmt.setString(1, "USER_UPDATE");
stmt.setString(2, currentUser.id());
stmt.setString(3, userId);
stmt.setString(4, txContext.getTransactionId());
stmt.executeUpdate();
} catch (SQLException e) {
throw new DataAccessException("Failed to log audit", e);
}
}
public void logOrderCreation(Order order, User currentUser, String transactionId) {
// Similar implementation...
}
}
// Other DAO classes (simplified)
public static class OrderDAO {
public Order createOrder(OrderRequest request, User user) {
// Implementation...
return new Order("order-123", request.productId(), request.quantity());
}
}
public static class InventoryDAO {
public boolean hasSufficientStock(String productId, int quantity) {
// Implementation...
return true;
}
public void reduceStock(String productId, int quantity) {
// Implementation...
}
}
public static class PaymentDAO {
public PaymentResult processPayment(String paymentMethod, double amount) {
// Implementation...
return new PaymentResult(true, null);
}
}
// Functional interface for transaction callbacks
@FunctionalInterface
public interface TransactionCallback<T> {
T doInTransaction();
}
// Record definitions
record User(String id, String name) {}
record UserProfile(String name, String email) {}
record OrderRequest(String productId, int quantity, String paymentMethod, double amount) {}
record Order(String id, String productId, int quantity) {}
record PaymentResult(boolean success, String errorMessage) {}
// Exception classes
public static class TransactionException extends RuntimeException {
public TransactionException(String message) { super(message); }
public TransactionException(String message, Throwable cause) { super(message, cause); }
}
public static class DataAccessException extends RuntimeException {
public DataAccessException(String message, Throwable cause) { super(message, cause); }
}
public static class InsufficientStockException extends RuntimeException {
public InsufficientStockException(String message) { super(message); }
}
public static class PaymentException extends RuntimeException {
public PaymentException(String message) { super(message); }
}
// Mock DataSource
public static class DataSource {
public static Connection getConnection() throws SQLException {
// In real application, return from connection pool
return null; // Simplified
}
}
}
Advanced Patterns
1. Scoped Value with Fallback
public class ScopedValueWithFallback {
private static final ScopedValue<Configuration> CONFIG = ScopedValue.newInstance();
private static final Configuration DEFAULT_CONFIG = new Configuration("default", "info");
public static Configuration getCurrentConfig() {
return CONFIG.orElse(DEFAULT_CONFIG);
}
public static void withConfig(Configuration config, Runnable task) {
ScopedValue.where(CONFIG, config).run(task);
}
public static <T> T withConfig(Configuration config, Supplier<T> task) {
return ScopedValue.where(CONFIG, config).call(task);
}
// Usage
public static void main(String[] args) {
// Use default config
System.out.println("Default config: " + getCurrentConfig());
// Use scoped config
Configuration customConfig = new Configuration("custom", "debug");
withConfig(customConfig, () -> {
System.out.println("Scoped config: " + getCurrentConfig());
// Nested with different config
withConfig(new Configuration("nested", "trace"), () -> {
System.out.println("Nested config: " + getCurrentConfig());
});
// Back to outer scoped config
System.out.println("Back to scoped config: " + getCurrentConfig());
});
// Back to default
System.out.println("Back to default: " + getCurrentConfig());
}
record Configuration(String name, String logLevel) {}
}
2. Scoped Value Validation
public class ScopedValueValidation {
private static final ScopedValue<UserSession> SESSION = ScopedValue.newInstance();
public static void requireAuthenticated() {
if (!SESSION.isBound()) {
throw new IllegalStateException("No user session bound");
}
UserSession session = SESSION.get();
if (!session.isValid()) {
throw new SecurityException("User session is invalid");
}
}
public static void requirePermission(String permission) {
requireAuthenticated();
UserSession session = SESSION.get();
if (!session.hasPermission(permission)) {
throw new SecurityException("Insufficient permissions: " + permission);
}
}
public static void withSession(UserSession session, Runnable task) {
if (!session.isValid()) {
throw new IllegalArgumentException("Invalid session");
}
ScopedValue.where(SESSION, session).run(task);
}
public static class UserSession {
private final String userId;
private final Set<String> permissions;
private final Instant expiry;
public UserSession(String userId, Set<String> permissions, Duration duration) {
this.userId = userId;
this.permissions = Set.copyOf(permissions);
this.expiry = Instant.now().plus(duration);
}
public boolean isValid() {
return Instant.now().isBefore(expiry);
}
public boolean hasPermission(String permission) {
return permissions.contains(permission);
}
// Getters
public String getUserId() { return userId; }
public Set<String> getPermissions() { return permissions; }
public Instant getExpiry() { return expiry; }
}
}
Migration from Thread-Local
1. Thread-Local to Scoped Value Migration
public class ThreadLocalMigration {
// OLD: Thread-Local approach
private static final ThreadLocal<UserContext> THREAD_LOCAL_CONTEXT = new ThreadLocal<>();
public static void oldApproach() {
// Setting value
THREAD_LOCAL_CONTEXT.set(new UserContext("user1"));
try {
// Using value
UserContext context = THREAD_LOCAL_CONTEXT.get();
processRequest(context);
} finally {
// Must remember to clean up!
THREAD_LOCAL_CONTEXT.remove();
}
}
// NEW: Scoped Value approach
private static final ScopedValue<UserContext> SCOPED_CONTEXT = ScopedValue.newInstance();
public static void newApproach() {
// Setting value with automatic cleanup
ScopedValue.where(SCOPED_CONTEXT, new UserContext("user1"))
.run(() -> {
// Using value
UserContext context = SCOPED_CONTEXT.get();
processRequest(context);
});
// Automatically cleaned up!
}
// Migrating InheritableThreadLocal
private static final InheritableThreadLocal<UserContext> INHERITABLE_CONTEXT =
new InheritableThreadLocal<>();
private static final ScopedValue<UserContext> SCOPED_INHERITABLE_CONTEXT =
ScopedValue.newInstance();
public static void migratInheritable() {
// OLD: Manual inheritance
INHERITABLE_CONTEXT.set(new UserContext("parent"));
Thread childThread = new Thread(() -> {
UserContext inherited = INHERITABLE_CONTEXT.get();
// Use inherited context
});
childThread.start();
// NEW: Automatic inheritance with StructuredTaskScope
ScopedValue.where(SCOPED_INHERITABLE_CONTEXT, new UserContext("parent"))
.run(() -> {
try (var scope = new StructuredTaskScope<String>()) {
scope.fork(() -> {
// Automatically inherits scoped values
UserContext inherited = SCOPED_INHERITABLE_CONTEXT.get();
return "done";
});
scope.join();
}
});
}
record UserContext(String userId) {}
private static void processRequest(UserContext context) {
// Processing logic
}
}
Best Practices
- Use for Immutable Data: Scoped values work best with immutable objects
- Keep Scope Small: Bind values for the minimal necessary scope
- Prefer over ThreadLocal: Use scoped values for new code
- Combine with Structured Concurrency: Perfect companion for structured task scopes
- Validate Usage: Check if values are bound when required
- Document Dependencies: Clearly document which scoped values your methods expect
Performance Benefits
- Faster access than ThreadLocal
- No memory leaks due to automatic cleanup
- Better optimizations by the JVM
- Automatic inheritance in structured concurrency
Scoped Values provide a modern, safe, and efficient alternative to Thread-Local variables, especially when combined with Java's structured concurrency features.