Сегодня вы узнаете о ThreadLocal, одном из распространенных классов, используемых при разработке Java-приложений.
Коротко: ThreadLocal дает каждому потоку собственную копию значения, поэтому потоки не мешают друг другу и синхронизация для такой переменной не нужна. Хранятся эти копии не внутри самого ThreadLocal, а в карте ThreadLocalMap у каждого потока, где ключом служит сам объект ThreadLocal. Главное практическое правило: в пуле потоков значение надо убирать вызовом remove(), иначе оно переживет задачу и останется висеть в потоке.
![Группа аквалангистов погружается в синей воде, заставка к статье о ThreadLocal]()
![Вывод программы: пять строк Thread number and its local value с номером потока и его значением, порядок строк не совпадает с порядком запуска потоков]()
![Схема: внутри потока лежит ThreadLocalMap, в ней записи Entry, где ключами служат объекты ThreadLocalA и ThreadLocalB, а значениями ValueA и ValueB]()
![Схема связей: у одного потока один ThreadLocalMap, а в нем N записей Entry, каждая из которых пара ThreadLocal и значение]()
Кратко
- Один объект ThreadLocal в разных потоках хранит разные значения, и поток видит только свое.
- Это способ обойтись без синхронизации там, где данные и не нужно делить между потоками.
- Значения лежат в карте ThreadLocalMap внутри самого потока, а ThreadLocal работает лишь ключом к ним.
- Если значение не задано, get() вернет null. Значение по умолчанию задают через ThreadLocal.withInitial().
- В пуле потоков обязательно вызывайте remove() в блоке finally: поток живет дольше задачи, и забытое значение утечет вместе с ним.
- С JDK 25 у класса появилась альтернатива, ScopedValue, которую документация советует предпочитать для односторонней передачи данных.
Что такое ThreadLocal?
Класс ThreadLocal хранит локальные переменные для потоков. Эти переменные изолированы между разными потоками и могут быть доступны только своему собственному потоку. Варианты применения ThreadLocal:- Изоляция данных между потоками.
- Управление сессиями для подключения к базе данных.
- Хранение транзакционной информации потока.
Как использовать ThreadLocal?
Давайте рассмотрим простой пример.public static void main(String[] args) {
//Создаем ThreadLocal
ThreadLocal<String> local = new ThreadLocal<>();
//Создаем новый класс Random
Random random = new Random();
//Создаем 5 потоков
IntStream.range(0, 5).forEach(a-> new Thread(()-> {
//Присваиваем значение каждому потоку
local.set(a+" "+random.nextInt(100));
System.out.println("Thread number and its local value "+ local.get());
}).start());
}
В приведенном выше коде мы создаем класс ThreadLocal, создаем 5 потоков, присваиваем значение ThreadLocal в каждом потоке и выводим на печать. При выводе получаем:
Что под капотом?
Если посмотреть внимательно, то можно понять, что в ThreadLocal из этого примера кода есть два важных метода.public T get() {}
public void set (T value) {}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
Метод setter сначала получает текущий поток и вызывает метод getMap() для получения класса ThreadLocalMap. Если map существует, возьмите текущий поток t в качестве ключа, входной параметр в качестве значения и установите пару {key:value} в map. Если нет, то создайте map.
Теперь у вас может появиться вопрос: что такое ThreadLocalMap?
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
ThreadLocalMap это внутренний статический класс в ThreadLocal, определяющий класс Entry для хранения данных. Entry использует экземпляр ThreadLocal в качестве ключа и устанавливает значение, которое мы передаем.
Если на данном этапе это звучит слишком запутанно, просто запомните, что именно класс Entry в ThreadLocalMap выполняет фактическое хранение значений.
Чтобы получить данные из ThreadLocal, мы используем метод getter:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
В методе getter мы будем использовать currentThread в качестве ключа для получения ThreadLocalMap. Затем map получит getEntry() на основе экземпляра ThreadLocal и вернет экземпляр Entry, а затем сохраненное значение.
Вот схема, которая поможет разобраться:
Каждый поток поддерживает ссылку на ThreadLocalMap.
ThreadLocalMap является внутренним статическим классом ThreadLocal и использует класс Entry для хранения.
Ключ ThreadLocalMap является экземпляром ThreadLocal и может иметь несколько ThreadLocal.
Сам ThreadLocal не хранит значение, но это ключ для потока, который поможет получить значение из ThreadLocalMap.
Методы ThreadLocal
В примерах выше участвовали только get() и set(), но у класса есть еще три метода, и без двух из них в реальном коде обойтись трудно:| Метод | Что делает | Когда нужен |
|---|---|---|
| get() | Возвращает значение текущего потока. Если его нет, вызывает initialValue() | основное чтение |
| set(T value) | Записывает значение для текущего потока | основная запись |
| remove() | Удаляет значение текущего потока | обязательно в пуле потоков, лучше в блоке finally |
| withInitial(Supplier) | Создает ThreadLocal со значением по умолчанию, появился в Java 8 | когда не хочется получать null при первом чтении |
| initialValue() | Защищенный метод, который возвращает начальное значение; по умолчанию это null | старый способ задать значение по умолчанию, через наследование |
Современная альтернатива: ScopedValue
В JDK 25 в язык добавили класс ScopedValue, и его документация прямо советует предпочитать его ThreadLocal там, где данные передаются в одну сторону, вглубь вызовов, без параметров метода. В том же примечании перечислены три претензии к ThreadLocal: значение может переписать любой вызванный код, время жизни значения ничем не ограничено, пока его не удалили вручную, а наследование значений дочерними потоками требует копирования всей карты. Выглядит это так:public class Demo {
// Значение обычно объявляют статической константой
private static final ScopedValue<String> USER = ScopedValue.newInstance();
public static void main(String[] args) {
// Значение связано только на время выполнения run
ScopedValue.where(USER, "duke").run(() -> handleRequest());
}
private static void handleRequest() {
// Параметр сюда никто не передавал, а значение доступно
System.out.println("Обрабатываем запрос пользователя " + USER.get());
// Вывод: Обрабатываем запрос пользователя duke
}
}
Разница с ThreadLocal в двух вещах. Во-первых, значение связывается не навсегда, а только на время выполнения кода, переданного в run(): как только он завершился, связь исчезает сама, и никакого remove() звать не нужно. Во-вторых, связать значение может только тот, кто вызывает where(), а вызванный код может его лишь прочитать.
Отсюда и поведение get(): если значение не связано, метод не вернет null, а бросит NoSuchElementException. Проверить связь заранее можно методом isBound(), подставить запасной вариант через orElse(). А связать несколько значений сразу позволяет цепочка: ScopedValue.where(k1, v1).where(k2, v2).run(...).
Значит ли это, что про ThreadLocal пора забыть? Нет. ScopedValue закрывает самый частый сценарий, передачу данных вглубь вызовов, но доступен он только с JDK 25, а его значение нельзя менять по ходу дела. Если данные нужно именно накапливать или переписывать в пределах потока, ThreadLocal остается на месте.
Вопросы и ответы
Что такое ThreadLocal простыми словами?
Это переменная, у которой в каждом потоке свое значение. Вы объявляете один объектThreadLocal, но когда поток пишет в него через set(), значение попадает в личную карту именно этого потока. Другой поток, обратившись к тому же объекту, увидит свое значение или null, если ничего не записывал.
Зачем нужен ThreadLocal, если есть synchronized?
Это решения двух разных задач.synchronized нужен, когда потоки работают с общими данными и им надо договариваться об очереди. ThreadLocal убирает саму необходимость договариваться: общих данных больше нет, у каждого потока своя копия. Поэтому он и не стоит ничего по скорости, но и не годится там, где потокам действительно нужно обмениваться информацией.
Почему ThreadLocal может привести к утечке памяти?
Из-за несимметричных ссылок внутриEntry: ключ там слабая ссылка на объект ThreadLocal, а значение обычная, сильная. Когда сам ThreadLocal становится недоступен, ключ обнуляется, но значение продолжает лежать в карте потока. Если поток короткоживущий, проблемы нет: он умрет и заберет карту. В пуле потоков поток не умирает, и забытые значения копятся до OutOfMemoryError.
Когда нужно вызывать remove()?
Всегда, когда код выполняется в пуле потоков: в сервлетах, в обработчиках запросов, в задачах дляExecutorService. Правильное место для вызова это блок finally, чтобы значение удалялось даже при исключении. В короткой программе, где поток создается под одну задачу и завершается, вызов remove() не обязателен.
Как задать значение по умолчанию, чтобы get() не возвращал null?
Проще всего создать переменную фабричным методомThreadLocal.withInitial(), передав ему лямбду, которая вернет начальное значение. Метод появился в Java 8 и заменяет старый прием с анонимным наследником и переопределением initialValue(). Начальное значение вычисляется лениво, при первом обращении потока к get().
Есть ли в современной Java замена ThreadLocal?
Да, в JDK 25 появился классScopedValue. Документация советует предпочитать его там, где данные передаются в одну сторону, вглубь вызовов, без параметров метода. У такого значения ограниченное время жизни: оно существует только внутри своей области и не требует ручной очистки, поэтому и утечек, свойственных ThreadLocal, у него нет.
Читайте также: