У цій лекції ми поговоримо про роботу із класом java.lang.ThreadLocal<> загалом і про те, як із ним працювати у багатопотоковому середовищі.

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

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

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

Конструктор Дія
ThreadLocal() Створює порожню змінну в 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-змінних. Ключами таблиці є посилання на об'єкти класу 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 для використання. Корисно в застосунках, де кілька потоків використовують випадкові числа паралельно в пулах потоків.
Це батьківський клас. Це дочірній клас.