Рубрики
Без рубрики

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

Краткое и практическое руководство по использованию ThreadLocal для хранения данных, специфичных для потоков, в Java.

Автор оригинала: baeldung.

1. Обзор

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

2. ThreadLocal API

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

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

ThreadLocal threadLocalValue = new ThreadLocal<>();

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

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

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

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

ThreadLocal threadLocal = 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 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
}

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

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

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

  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 .