В этой лекции мы поговорим о работе с классом java.lang.ThreadLocal<> в целом и о том, как с ним работать в многопоточной среде.

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

Если разобрать работу класса более подробно, можем представить себе Map вида поток→значение, который при использовании обращается к значению для текущего потока.

Конструктор класса ThreadLocal

Конструктор Действие
ThreadLocal() Cоздает пустую переменную в Java

Методы

Метод Действие
get() Возвращает значение локальной переменной текущего потока
set() Устанавливает значение локальной переменной для текущего потока
remove() Удаляет значение локальной переменной текущего потока
ThreadLocal.withInitial() Дополнительный фабричный метод, который устанавливает начальное значение

get() & set()

Давайте напишем пример, где создадим два счётчика. Первая, обычная переменная будет для подсчёта количества потоков, а вторую мы обернем в ThreadLocal и посмотрим, как они будут работать совместно. Для начала напишем класс ThreadDemo, унаследованный от Runnable, где будут хранится наши данные и основной метод run(), а также добавим метод для вывода счётчиков на экран:


class ThreadDemo implements Runnable {

    int counter;
    ThreadLocal<Integer> threadLocalCounter = new ThreadLocal<>();

    public void run() {
        counter++;

        if(threadLocalCounter.get() != null) {
            threadLocalCounter.set(threadLocalCounter.get() + 1);
        } else {
            threadLocalCounter.set(0);
        }
        printCounters();
    }

    public void printCounters(){
        System.out.println("Counter: " + counter);
        System.out.println("threadLocalCounter: " + threadLocalCounter.get());
    }
}

С каждым запуском нашего класса мы будем увеличивать переменную counter, и дополнительно будем вызвать метод get() для получения данных из ThreadLocal-переменной. Если в новом потоке не будет данных, то будем устанавливать 0, а если данные будут — увеличим их на единицу. И давайте напишем наш класс main:


public static void main(String[] args) {
    ThreadDemo threadDemo = new ThreadDemo();

    Thread t1 = new Thread(threadDemo);
    Thread t2 = new Thread(threadDemo);
    Thread t3 = new Thread(threadDemo);

    t1.start();
    t2.start();
    t3.start();

}

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

Counter: 1
Counter: 2
Counter: 3
threadLocalCounter: 0
threadLocalCounter: 0
threadLocalCounter: 0

Process finished with exit code 0

remove()

Чтобы понять работу метода remove, мы просто немного изменим код класса ThreadDemo:


if(threadLocalCounter.get() != null) {
      threadLocalCounter.set(threadLocalCounter.get() + 1);
  } else {
      if (counter % 2 == 0) {
          threadLocalCounter.remove();
      } else {
          threadLocalCounter.set(0);
      }
  }

В этом коде мы определим, что если это счётчик потока — четное число, то мы вызовем метод remove() у нашей ThreadLocal-переменной. Результат кода:

Counter: 3
threadLocalCounter: 0
Counter: 2
threadLocalCounter: null
Counter: 1
threadLocalCounter: 0

Process finished with exit code 0

И здесь мы легко можем увидеть, что ThreadLocal-переменная во втором потоке равна null.

ThreadLocal.withInitial()

Этот метод создает локальную переменную потока.

Реализация класса ThreadDemo:


class ThreadDemo implements Runnable {

    int counter;
    ThreadLocal<Integer> threadLocalCounter = ThreadLocal.withInitial(() -> 1);

    public void run() {
        counter++;
        printCounters();
    }

    public void printCounters(){
        System.out.println("Counter: " + counter);
        System.out.println("threadLocalCounter: " + threadLocalCounter.get());
    }
}

И мы можем посмотреть на результат нашего кода:

Counter: 1
Counter: 2
Counter: 3
threadLocalCounter: 1
threadLocalCounter: 1
threadLocalCounter: 1

Process finished with exit code 0

Зачем нам использовать такие переменные?

ThreadLocal предоставляет абстракцию над локальными переменными по отношению к потоку исполнения java.lang.Thread.

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

У каждого потока, то есть экземпляра класса Thread, есть ассоциированная с ним таблица ThreadLocal-переменных. Ключами таблицы являются cсылки на объекты класса ThreadLocal, а значениями — ссылки на объекты, “захваченные” ThreadLocal-переменными.

Почему генерация случайных чисел через Random не подходит для многопоточных приложений?

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

Для этого в JDK 7 представили класс java.util.concurrent.ThreadLocalRandom для генерации случайных чисел в многопоточной среде. Он состоит из двух классов: ThreadLocal и Random.

Случайное число, полученное одним потоком, не зависит от другого потока, тогда как java.util.Random предоставляет случайные числа глобально. Кроме того, в отличие от Random, ThreadLocalRandom не поддерживает явное задание начального значения. Вместо этого он переопределяет метод setSeed(), унаследованный от Random, чтобы всегда вызывать UnsupportedOperationException при вызове.

Давайте рассмотрим методы ThreadLocalRandom:

Метод Дейстивие
ThreadLocalRandom current() Возвращает ThreadLocalRandom текущего потока.
int next(int bits) Генерирует следующее псевдослучайное число.
double nextDouble(double least, double bound) Возвращает псевдослучайное, равномерно распределенное значение между заданным наименьшим (least) значением (включительно) и граничным (bound) (исключительно).
int nextInt(int least, int bound) Возвращает псевдослучайное, равномерно распределенное значение между заданным наименьшим значением (включительно) и связанным (исключительно).
long nextLong(long n) Возвращает псевдослучайное, равномерно распределенное значение между 0 (включительно) и заданным значением (исключительно).
long nextLong(long least, long bound) Возвращает псевдослучайное, равномерно распределенное значение между заданным наименьшим значением (включительно) и связанным (исключительно).
void setSeed(long seed) Выдает исключение UnsupportedOperationException. Установка начального числа в этом генераторе не поддерживается.

Получение случайных данных с помощью ThreadLocalRandom.current()

ThreadLocalRandom представляет собой комбинацию классов ThreadLocal и Random. Таким образом он достигает лучшей производительности в многопоточной среде, просто избегая любого параллельного доступа к экземплярам Random.

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


import java.util.concurrent.ThreadLocalRandom;

class RandomNumbers extends Thread {

    public void run() {
        try {
            int bound = 100;
            int result = ThreadLocalRandom.current().nextInt(bound);
            System.out.println("Thread " + Thread.currentThread().getId() + " generated " + result);
        }
        catch (Exception e) {
            System.out.println("Exception");
        }
    }

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();

				for (int i = 0; i < 10; i++) {
            RandomNumbers randomNumbers = new RandomNumbers();
            randomNumbers.start();
        }

        long endTime = System.currentTimeMillis();

        System.out.println("Time taken: " + (endTime - startTime));
    }
}

Результат нашей программы:

Time taken: 1
Thread 17 generated 13
Thread 18 generated 41
Thread 16 generated 99
Thread 19 generated 25
Thread 23 generated 33
Thread 24 generated 21
Thread 15 generated 15
Thread 21 generated 28
Thread 22 generated 97
Thread 20 generated 33

А теперь изменим наш класс RandomNumbers и будем использовать в нем Random:


int result = new Random().nextInt(bound);
Time taken: 5
Thread 20 generated 48
Thread 19 generated 57
Thread 18 generated 90
Thread 22 generated 43
Thread 24 generated 7
Thread 23 generated 63
Thread 15 generated 2
Thread 16 generated 40
Thread 17 generated 29
Thread 21 generated 12

Важно отметить! В текущих тестах результаты иногда совпадали и были разными по значению. Но если мы будем говорить о большем количестве потоков (например, 100), результат будет выглядеть вот так:

Random - 19-25 mls
ThreadLocalRandom - 17-19 mls

Соответсвенно, чем больше потоков в нашем приложении, тем сильнее использование класса Random для многопоточности будет уменьшать производительность.

Подведем итог и повторим еще раз различия между классом Random и ThreadLocalRandom:

Random ThreadLocalRandom
Если разные потоки используют один и тот же экземпляр Random, это приводит к конфликтам и отрицательно влияет на производительность. Споров и проблем нет, потому что сгенерированные случайные числа являются локальными для текущего потока.
Использует линейную конгруэнтную формулу для изменения начального значения. Генератор случайных чисел инициализируется с использованием внутренне сгенерированного начального числа.
Полезно в приложениях, где каждый поток имеет собственный набор экземпляров Random для использования. Полезно в приложениях, где несколько потоков используют случайные числа параллельно в пулах потоков.
Это родительский класс. Это дочерний класс.