1. Обзор

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

2. ThreadLocal API

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

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

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

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

Из-за этого факта, когда мы вызываем метод get для threadLocalValue, мы получим значение Integer для запрашивающего потока.

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

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

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

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

threadLocal.remove();

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

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

Рассмотрим программу, которая должна хранить специфические для пользователя данные контекста для каждого идентификатора пользователя.

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

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

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

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

Метод 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'}

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

5. Не используйте ThreadLocal с ExecutorService

Если мы хотим использовать ExecutorService и передать ему Runnable, использование ThreadLocal даст недетерминированные результаты, потому что у нас нет гарантии, что каждое действие Runnable для данного userId будет обрабатываться одним и тем же потоком при каждом его выполнении.

Из-за этого наш ThreadLocal будет разделен между различными идентификаторами пользователя. Вот почему мы не должны использовать TheadLocal вместе с ExecutorService. Его следует использовать только тогда, когда мы имеем полный контроль над тем, какой поток выберет какое выполняемое действие выполнить.

6. Заключение

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