Введение в ThreadLocal в Java
1. обзор
В этой статье мы рассмотрим конструкциюThreadLocal из пакетаjava.lang. Это дает нам возможность хранить данные индивидуально для текущего потока - и просто оборачивать их в особый тип объекта.
2. ThreadLocal API
КонструкцияTheadLocal позволяет нам хранить данные, которые будутaccessible only наa specific thread.
Допустим, мы хотим иметь значениеInteger, которое будет связано с конкретным потоком:
ThreadLocal threadLocalValue = new ThreadLocal<>();
Затем, когда мы хотим использовать это значение из потока, нам нужно только вызвать методget() илиset(). Проще говоря, мы можем думать, чтоThreadLocal хранит данные внутри карты - с потоком в качестве ключа.
В связи с этим, когда мы вызываем методget() дляthreadLocalValue, мы получим значениеInteger для запрашивающего потока:
threadLocalValue.set(1);
Integer result = threadLocalValue.get();
Мы можем создать экземплярThreadLocal, используя статический методwithInitial() и передав ему поставщика:
ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 1);
Чтобы удалить значение изThreadLocal, мы можем вызвать методremove():
threadLocal.remove();
Чтобы увидеть, как правильно использоватьThreadLocal, сначала мы рассмотрим пример, который не используетThreadLocal, а затем перепишем наш пример, чтобы использовать эту конструкцию.
3. Хранение пользовательских данных на карте
Давайте рассмотрим программу, которая должна хранить пользовательские данныеContext для данного идентификатора пользователя:
public class Context {
private String userName;
public Context(String userName) {
this.userName = userName;
}
}
Мы хотим иметь один поток для каждого идентификатора пользователя. Мы создадим классSharedMapWithUserContext, реализующий интерфейсRunnable. Реализация в методеrun() вызывает некоторую базу данных через классUserRepository, который возвращает объектContext для данногоuserId.
Затем мы сохраняем этот контекст вConcurentHashMap с ключомuserId:
public class SharedMapWithUserContext implements Runnable {
public static Map userContextPerUserId
= 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
}
Мы можем легко протестировать наш код, создав и запустив два потока для двух разныхuserIds и заявив, что у нас есть две записи в картеuserContextPerUserId:
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
Мы можем переписать наш пример, чтобы сохранить экземпляр пользователяContext, используяThreadLocal. У каждого потока будет свой экземплярThreadLocal.
При использованииThreadLocal нам нужно быть очень осторожными, потому что каждый экземплярThreadLocal связан с определенным потоком. В нашем примере у нас есть выделенный поток для каждого конкретногоuserId, и этот поток создается нами, поэтому мы полностью контролируем его.
Методrun() получит пользовательский контекст и сохранит его в переменнойThreadLocal с помощью методаset():
public class ThreadLocalWithUserContext implements Runnable {
private static ThreadLocal userContext
= 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. Не используйтеThreadLocal сExecutorService
Если мы хотим использоватьExecutorService и передать емуRunnable, использованиеThreadLocal даст недетерминированные результаты - потому что у нас нет гарантии, что каждое действиеRunnable для данногоuserId будет обрабатываться одним и тем же потоком каждый раз при его выполнении.
Из-за этого нашThreadLocal будет разделен между разнымиuserIds.. Поэтому мы не должны использоватьTheadLocal вместе сExecutorService.. Его следует использовать только тогда, когда у нас есть полный контроль над какой поток выберет выполняемое действие для выполнения.
6. Заключение
В этой быстрой статье мы рассмотрели конструкциюThreadLocal. Мы реализовали логику, которая используетConcurrentHashMap, совместно используемый потоками, для хранения контекста, связанного с конкретнымuserId.. Затем мы переписали наш пример, чтобы использоватьThreadLocal для хранения данных, связанных с конкретныйuserId и с конкретным потоком.
Реализация всех этих примеров и фрагментов кода можно найти вGitHub project - это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.