В чём разница между ScopedValue и ThreadLocal?
ScopedValue (JEP 464, preview в Java 21+) и ThreadLocal решают одну задачу — передача контекстных данных через стек вызовов без явной передачи параметров. Однако они фундаментально различаются в дизайне.
ThreadLocal — мутабельный контекст с ручным управлением:
Пример
private static final ThreadLocal<String> USER_ID = new ThreadLocal<>();
void handleRequest(String userId) {
USER_ID.set(userId);
try {
processRequest(); // может читать USER_ID.get() на любом уровне вложенности
} finally {
USER_ID.remove(); // ОБЯЗАТЕЛЬНО очистить!
}
}
ScopedValue — иммутабельный контекст с автоматическим временем жизни:
Пример
private static final ScopedValue<String> USER_ID = ScopedValue.newInstance();
void handleRequest(String userId) {
ScopedValue.runWhere(USER_ID, userId, () -> {
processRequest(); // может читать USER_ID.get()
});
// После выхода из runWhere значение автоматически недоступно
}
Сравнительная таблица:
| Характеристика | ThreadLocal |
ScopedValue |
|---|---|---|
| Изменяемость | Мутабельный (set/get/remove) |
Иммутабельный (значение задаётся раз для scope) |
| Время жизни | Пока жив поток или пока не вызван remove() |
Ограничено блоком runWhere/callWhere |
| Наследование дочерними потоками | InheritableThreadLocal — копирование |
Автоматически в StructuredTaskScope.fork() |
| Производительность | Хэш-таблица в каждом потоке | Оптимизирован для чтения |
| Утечки памяти | Частая проблема (забытый remove()) |
Невозможны — автоматическая очистка |
| Virtual Threads | Проблематичен при миллионах потоков | Создан для Virtual Threads |
Код: ScopedValue с StructuredTaskScope
private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
String handleRequest(String requestId) throws Exception {
return ScopedValue.callWhere(REQUEST_ID, requestId, () -> {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<String> userData = scope.fork(() -> {
String id = REQUEST_ID.get(); // доступно!
return fetchUser(id);
});
Subtask<String> orderData = scope.fork(() -> {
String id = REQUEST_ID.get(); // доступно!
return fetchOrders(id);
});
scope.join();
scope.throwIfFailed();
return userData.get() + " : " + orderData.get();
}
});
}
Перебиндинг во вложенном scope:
ScopedValue можно «переопределить» во вложенном блоке — внутренний scope видит новое значение, внешний — старое:
Пример
ScopedValue.runWhere(USER_ID, "admin", () -> {
System.out.println(USER_ID.get()); // "admin"
ScopedValue.runWhere(USER_ID, "superadmin", () -> {
System.out.println(USER_ID.get()); // "superadmin"
});
System.out.println(USER_ID.get()); // "admin" — восстановлено
});
Когда по-прежнему нужен ThreadLocal:
- Значение должно изменяться в процессе выполнения (накопление данных).
- Код работает на Java < 21.
Частые ошибки:
ThreadLocalс Virtual Threads безremove()— утечка памяти при миллионах потоков.ScopedValue.get()внеrunWhere/callWhere— броситNoSuchElementException.InheritableThreadLocalс пулом потоков — «чужое» значение при переиспользовании потока.
Аналогия:
ThreadLocal— это доска с записями на рабочем столе каждого сотрудника: он может писать и стирать что угодно, и если забудет стереть — следующий за этим столом увидит чужие записи.ScopedValue— это бейджик, который надевается на время совещания и снимается автоматически при выходе.
На собеседовании ключевой вопрос: «Зачем нужен
ScopedValue, если естьThreadLocal?» Ответ:ScopedValueиммутабелен (нет race condition), не требует ручной очистки (нет утечек) и оптимизирован для Virtual Threads (нет хэш-таблицы на поток).