Перейти к основному содержимому

Введение в ThreadLocal в Java

· 5 мин. чтения

1. Обзор

В этом руководстве мы рассмотрим конструкцию ThreadLocal из пакета java.lang . Это дает нам возможность хранить данные отдельно для текущего потока и просто оборачивать их в объект специального типа.

2. API ThreadLocal

Конструкция TheadLocal позволяет нам хранить данные, которые будут доступны только определенному потоку .

Допустим, мы хотим иметь значение Integer , которое будет связано с конкретным потоком:

ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();

Затем, когда мы хотим использовать это значение из потока, нам нужно только вызвать метод get() или set() . Проще говоря, мы можем представить, что ThreadLocal хранит данные внутри карты с потоком в качестве ключа.

В результате, когда мы вызываем метод get() для threadLocalValue , мы получаем целочисленное значение для запрашивающего потока:

threadLocalValue.set(1);
Integer result = threadLocalValue.get();

Мы можем создать экземпляр ThreadLocal , используя статический метод withInitial() и передав ему поставщика:

ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);

Чтобы удалить значение из ThreadLocal , мы можем вызвать метод remove() :

threadLocal.remove();

Чтобы увидеть, как правильно использовать ThreadLocal , мы сначала рассмотрим пример, в котором ThreadLocal не используется , а затем перепишем наш пример, чтобы использовать эту конструкцию.

3. Хранение пользовательских данных на карте

Давайте рассмотрим программу, которая должна хранить пользовательские данные контекста для заданного идентификатора пользователя:

public class Context {
private String userName;

public Context(String userName) {
this.userName = userName;
}
}

Мы хотим иметь один поток для каждого идентификатора пользователя. Мы создадим класс SharedMapWithUserContext , реализующий интерфейс Runnable . Реализация в методе run() вызывает некоторую базу данных через класс UserRepository , который возвращает объект Context для заданного идентификатора пользователя .

Затем мы сохраняем этот контекст в ConcurentHashMap с ключом userId :

public class SharedMapWithUserContext implements Runnable {

public static Map<Integer, Context> 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
}

Мы можем легко протестировать наш код, создав и запустив два потока для двух разных идентификаторов пользователей и утверждая, что у нас есть две записи в карте 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

Мы можем переписать наш пример, чтобы сохранить экземпляр пользовательского контекста с помощью ThreadLocal . Каждый поток будет иметь свой собственный экземпляр ThreadLocal .

При использовании ThreadLocal нам нужно быть очень осторожными, потому что каждый экземпляр ThreadLocal связан с определенным потоком. В нашем примере у нас есть выделенный поток для каждого конкретного userId , и этот поток создается нами, поэтому мы имеем над ним полный контроль.

Метод run() извлечет пользовательский контекст и сохранит его в переменной ThreadLocal с помощью метода set() :

public class ThreadLocalWithUserContext implements Runnable {

private static ThreadLocal<Context> 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
}

Мы можем проверить это, запустив два потока, которые будут выполнять действие для заданного идентификатора пользователя :

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'}

Мы видим, что у каждого из пользователей есть свой Context .

5. ThreadLocal и пулы потоков

ThreadLocal предоставляет простой в использовании API для ограничения некоторых значений для каждого потока. Это разумный способ достижения потокобезопасности в Java. Однако мы должны быть особенно осторожны, когда используем ThreadLocal и пулы потоков вместе.

Чтобы лучше понять это возможное предостережение, давайте рассмотрим следующий сценарий:

  1. Сначала приложение заимствует поток из пула.
  2. Затем он сохраняет некоторые значения, ограниченные потоком, в ThreadLocal текущего потока .
  3. После завершения текущего выполнения приложение возвращает заимствованный поток в пул.
  4. Через некоторое время приложение заимствует тот же поток для обработки другого запроса.
  5. Поскольку в прошлый раз приложение не выполнило необходимую очистку, оно может повторно использовать те же данные 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 .