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

Введение в 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, поэтому его должно быть легко импортировать и запускать как есть.