1. Обзор
В этой статье мы рассмотрим конструкцию ThreadLocal из пакета java.lang . Это дает нам возможность хранить данные отдельно для текущего потока – и просто переносить их в специальный тип объекта.
2. ThreadLocal API
Конструкция Threadlocal позволяет нам хранить данные, которые будут доступны только конкретному потоку .
Допустим, мы хотим иметь значение Integer , которое будет связано с конкретным потоком:
ThreadLocalthreadLocalValue = new ThreadLocal<>();
Далее, когда мы хотим использовать это значение из потока, нам нужно только вызвать метод get() или set () . Проще говоря, мы можем думать, что ThreadLocal хранит данные внутри карты – с потоком в качестве ключа.
В связи с этим, когда мы вызываем метод get() для ThreadLocal значения , мы получим Целое значение для запрашивающего потока:
threadLocalValue.set(1); Integer result = threadLocalValue.get();
Мы можем построить экземпляр ThreadLocal , используя метод with Initial() static и передав ему поставщика:
ThreadLocalthreadLocal = ThreadLocal.withInitial(() -> 1);
Чтобы удалить значение из ThreadLocal , мы можем вызвать метод remove() :
threadLocal.remove();
Чтобы увидеть, как правильно использовать ThreadLocal , во-первых, мы рассмотрим пример, в котором не используется ThreadLocal , затем мы перепишем наш пример, чтобы использовать эту конструкцию.
3. Хранение пользовательских данных на карте
Давайте рассмотрим программу, которая должна хранить пользовательские Контекстные данные для каждого заданного идентификатора пользователя:
public class Context { private String userName; public Context(String userName) { this.userName = userName; } }
Мы хотим иметь один поток на идентификатор пользователя. Мы создадим Общую карту С пользовательским контекстом классом, который реализует Запускаемый интерфейс. Реализация в методе run() вызывает некоторую базу данных через класс UserRepository , который возвращает объект Context для заданного userId .
Затем мы сохраняем этот контекст в Concurrenthashmap с ключом userId :
public class SharedMapWithUserContext implements Runnable { public static MapuserContextPerUserId = new ConcurrentHashMap<>(); private Integer userId; private UserRepository userRepository = new UserRepository(); @Override public void run() { String userName = userRepository.getUserNameForUserId(userId); userContextPerUserId.put(userId, new Context(userName)); } // standard constructor }
Мы можем легко протестировать ваш код, создав и запустив два потока для двух разных идентификаторов пользователей и утверждая, что у нас есть две записи в userContextPerUserId map:
SharedMapWithUserContext firstUser = new SharedMapWithUserContext(1); SharedMapWithUserContext secondUser = new SharedMapWithUserContext(2); new Thread(firstUser).start(); new Thread(secondUser).start(); assertEquals(SharedMapWithUserContext.userContextPerUserId.size(), 2);
4. Хранение пользовательских данных в ThreadLocal
Мы можем переписать наш пример, чтобы сохранить экземпляр user Context , используя ThreadLocal . Каждый поток будет иметь свой собственный экземпляр ThreadLocal .
При использовании ThreadLocal мы должны быть очень осторожны , потому что каждый экземпляр ThreadLocal связан с определенным потоком. В нашем примере у нас есть выделенный поток для каждого конкретного идентификатора пользователя , и этот поток создается нами, поэтому мы полностью контролируем его.
Метод run() извлекает пользовательский контекст и сохраняет его в переменной ThreadLocal с помощью метода set() :
public class ThreadLocalWithUserContext implements Runnable { private static ThreadLocaluserContext = new ThreadLocal<>(); private Integer userId; private UserRepository userRepository = new UserRepository(); @Override public void run() { String userName = userRepository.getUserNameForUserId(userId); userContext.set(new Context(userName)); System.out.println("thread context for given userId: " + userId + " is: " + userContext.get()); } // standard constructor }
Мы можем проверить это, запустив два потока, которые будут выполнять действие для данного userId :
ThreadLocalWithUserContext firstUser = new ThreadLocalWithUserContext(1); ThreadLocalWithUserContext secondUser = new ThreadLocalWithUserContext(2); new Thread(firstUser).start(); new Thread(secondUser).start();
После запуска этого кода мы увидим на стандартном выходе, что ThreadLocal был установлен для данного потока:
thread context for given userId: 1 is: Context{userNameSecret='18a78f8e-24d2-4abf-91d6-79eaa198123f'} thread context for given userId: 2 is: Context{userNameSecret='e19f6a0a-253e-423e-8b2b-bca1f471ae5c'}
Мы видим, что у каждого из пользователей есть свой собственный Контекст .
5. ThreadLocals и пулы потоков
ThreadLocal предоставляет простой в использовании API для ограничения некоторых значений для каждого потока. Это разумный способ достижения потокобезопасности в Java. Однако мы должны быть особенно осторожны, когда мы используем ThreadLocal s и пулы потоков вместе.
Чтобы лучше понять возможную оговорку, давайте рассмотрим следующий сценарий:
- Во-первых, приложение заимствует поток из пула.
- Затем он сохраняет некоторые ограниченные потоком значения в ThreadLocal текущего потока .
- После завершения текущего выполнения приложение возвращает заимствованный поток в пул.
- Через некоторое время приложение заимствует тот же поток для обработки другого запроса.
- Поскольку приложение не выполнило необходимые очистки в прошлый раз, оно может повторно использовать то же самое ThreadLocal данные для нового запроса.
Это может привести к неожиданным последствиям в сильно параллельных приложениях.
Один из способов решить эту проблему-вручную удалить каждый ThreadLocal , как только мы закончим его использовать. Поскольку этот подход требует тщательного анализа кода, он может быть подвержен ошибкам.
5.1. Расширение ThreadPoolExecutor
Как оказалось, можно расширить класс ThreadPoolExecutor и предоставить пользовательскую реализацию крючка для методов beforeExecute() и afterExecute () . Пул потоков вызовет метод beforeExecute() перед запуском чего-либо с использованием заимствованного потока. С другой стороны, он вызовет метод afterExecute() после выполнения нашей логики.
Поэтому мы можем расширить класс ThreadPoolExecutor и удалить данные ThreadLocal в методе afterExecute() :
public class ThreadLocalAwareThreadPool extends ThreadPoolExecutor { @Override protected void afterExecute(Runnable r, Throwable t) { // Call remove on each ThreadLocal } }
Если мы отправим наши запросы в эту реализацию ExecutorService , то мы можем быть уверены, что использование ThreadLocal и пулов потоков не создаст угрозы безопасности для нашего приложения.
6. Заключение
В этой краткой статье мы рассматривали конструкцию ThreadLocal . Мы реализовали логику, использующую ConcurrentHashMap , которая была разделена между потоками для хранения контекста, связанного с конкретным идентификатором пользователя. Далее мы переписали наш пример, чтобы использовать ThreadLocal для хранения данных, связанных с конкретным идентификатором пользователя и с конкретным потоком.
Реализацию всех этих примеров и фрагментов кода можно найти на GitHub .