В този урок ще говорим най-общо за работата с класа java.lang.ThreadLocal<> и How да го използвате в многонишкова среда.

Класът ThreadLocal се използва за съхраняване на променливи. Отличителна черта на този клас е, че той съхранява отделно независимо копие на стойност за всяка нишка, която го използва.

Задълбочавайки се в работата на класа, можем да си представим карта , която картографира нишки към стойности, от които текущата нишка взема подходящата стойност, когато трябва да я използва.

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

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

Методи

Метод Действие
получи() Връща стойността на локалната променлива на текущата нишка
комплект() Задава стойността на локалната променлива за текущата нишка
Премахване() Премахва стойността на локалната променлива на текущата нишка
ThreadLocal.withInitial() Допълнителен фабричен метод, който задава първоначалната стойност

приготви се()

Нека напишем пример, в който създаваме два брояча. Първата, обикновена променлива, ще бъде за преброяване на броя на нишките. Вторият ще увием в ThreadLocal . И ще видим How ще работят заедно. Първо, нека напишем клас 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());
    }
}

С всяко изпълнение на нашия клас ние увеличавамеброячпроменлива извикване на метода get() , за да получите данни от променливата ThreadLocal . Ако новата нишка няма данни, тогава ще я зададем на 0. Ако има данни, ще я увеличим с единица. И нека напишем нашия основен метод:


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 остава същата, независимо от нишката, която има достъп до нея, но броят на нишките нараства.

Брояч: 1
Брояч: 2
Брояч: 3
threadLocalCounter: 0
threadLocalCounter: 0
threadLocalCounter: 0

Процесът завърши с изходен code 0

Премахване()

За да разберем How работи методът за премахване , просто ще променим леко codeа в класа ThreadDemo :


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

В този code, ако броячът на нишките е четно число, тогава ще извикаме метода remove() на нашата променлива ThreadLocal . Резултат:

Брояч: 3
threadLocalCounter: 0
Брояч: 2
threadLocalCounter: null
Брояч: 1
threadLocalCounter: 0

Процесът завърши с изходен 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());
    }
}

И можем да погледнем резултата от нашия code:

Брояч: 1
Брояч: 2
Брояч: 3
threadLocalCounter: 1
threadLocalCounter: 1
threadLocalCounter: 1

Процесът завърши с изходен code 0

Защо трябва да използваме такива променливи?

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

ThreadLocal променливите се различават от обикновените по това, че всяка нишка има свой собствен, индивидуално инициализиран екземпляр на променливата, който е достъпен чрез методите get() и set() .

Всяка нишка, т.е. екземпляр на класа Thread , има карта на ThreadLocal променливи, свързани с нея. Ключовете на картата са препратки към ThreadLocal обекти, а стойностите са препратки към "придобити" ThreadLocal променливи.

Защо класът Random не е подходящ за генериране на произволни числа в многонишкови applications?

Използваме класа Random , за да получим произволни числа. Но работи ли също толкова добре в многонишкова среда? Всъщност не. Random не е подходящ за многонишкови среди, защото когато множество нишки имат достъп до клас едновременно, производителността страда.

За да се справи с този проблем, JDK 7 въведе класа java.util.concurrent.ThreadLocalRandom за генериране на произволни числа в многонишкова среда. Състои се от два класа: ThreadLocal и Random .

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

Нека да разгледаме методите на класа ThreadLocalRandom :

Метод Действие
ThreadLocalRandom current() Връща ThreadLocalRandom на текущата нишка.
int следващ (int битове) Генерира следващото псевдослучайно число.
двойно следващДвойно(двойно най-малко, двойно обвързано) Връща псевдослучайно число от равномерно разпределение между най-малко (включително) и ограничено (изключващо).
int nextInt(int най-малко, int обвързан) Връща псевдослучайно число от равномерно разпределение между най-малко (включително) и ограничено (изключващо).
дълъг следващ дълъг (дълъг n) Връща псевдослучайно число от равномерно разпределение между 0 (включително) и посочената стойност (изключително).
дълго следващо дълго (дълго най-малко, дълго обвързано) Връща псевдослучайно число от равномерно разпределение между най-малко (включително) и ограничено (изключващо).
void setSeed (дълго семе) Изхвърля UnsupportedOperationException . Този генератор не поддържа зареждане.

Получаване на произволни числа чрез ThreadLocalRandom.current()

ThreadLocalRandom е комбинация от класовете ThreadLocal и Random . Той постига по-добра производителност в многонишкова среда, като просто избягва всяHowъв едновременен достъп до екземпляри от класа Random .

Нека приложим пример, включващ множество нишки, и да видим How нашето приложение работи с класа 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));
    }
}

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

Отнето време: 1
Генерирана нишка 17 13
Генерирана нишка 18 41
Генерирана нишка 16 99
Генерирана нишка 19 25 Генерирана
нишка 23 33
Генерирана
нишка 24 21 Генерирана нишка 15 15 Генерирана
нишка 21 28 Генерирана
нишка 22 97 Генерирана
нишка 20 33

А сега нека променим нашия клас RandomNumbers и да използваме Random в него:


int result = new Random().nextInt(bound);
Отнето време: 5
Генерирана нишка 20 48
Генерирана нишка 19 57
Генерирана нишка 18 90 Генерирана
нишка 22 43
Генерирана нишка 24 7
Генерирана нишка 23 63
Генерирана нишка 15 2 Генерирана
нишка 16 40
Генерирана нишка 17 29 Генерирана
нишка 21 12

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

Случаен — 19-25 ms
ThreadLocalRandom — 17-19 ms

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

За да обобщим и повторим разликите между класовете Random и ThreadLocalRandom :

Случаен ThreadLocalRandom
Ако различни нишки използват едно и също копие на Random , ще има конфликти и производителността ще се влоши. Няма конфликти or проблеми, тъй като генерираните произволни числа са локални за текущата нишка.
Използва линейна конгруентна формула за промяна на първоначалната стойност. Генераторът на произволни числа се инициализира с помощта на вътрешно генерирано начало.
Полезно в applications, където всяка нишка използва свой собствен набор от случайни обекти. Полезно в applications, където множество нишки използват произволни числа паралелно в пулове нишки.
Това е родителски клас. Това е детски клас.