Podczas tego wykładu omówimy ogólnie pracę z klasą java.lang.ThreadLocal<> oraz sposób pracy z nią w środowisku wielowątkowym.

Klasa ThreadLocal służy do przechowywania zmiennych. Osobliwością tej klasy jest to, że przechowuje oddzielną, niezależną kopię wartości dla każdego wątku, który z niej korzysta.

Jeśli rozłożymy bardziej szczegółowo sposób działania klasy, możemy sobie wyobrazić mapę formularza strumień→wartość, która, gdy jest używana, odnosi się do wartości bieżącego strumienia.

Konstruktor klasy ThreadLocal

Konstruktor Działanie
WątekLokalny() Tworzy pustą zmienną w Javie

Metody

metoda Działanie
Dostawać() Zwraca wartość zmiennej lokalnej bieżącego wątku
ustawić() Ustawia wartość zmiennej lokalnej dla bieżącego wątku
usunąć() Usuwa wartość zmiennej lokalnej bieżącego wątku
ThreadLocal.withInitial() Opcjonalna metoda fabryczna, która ustawia wartość początkową

przygotować się()

Napiszmy przykład, w którym tworzymy dwa liczniki. Pierwsza, normalna zmienna będzie do zliczania liczby wątków, a drugą zawiniemy w ThreadLocal i zobaczymy, jak ze sobą współpracują. Najpierw napiszmy klasę ThreadDemo odziedziczoną po Runnable , w której będą przechowywane nasze dane i główną metodę run() oraz dodajmy metodę wyświetlania liczników na ekranie:


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());
    }
}

Z każdym uruchomieniem naszej klasy będziemy zwiększać zmiennąlada, a dodatkowo wywołamy metodę get() w celu pobrania danych ze zmiennej ThreadLocal . Jeśli w nowym strumieniu nie ma danych, to ustawimy go na 0, a jeśli są dane, zwiększymy go o jeden. I napiszmy naszą główną klasę :


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();

}

W wyniku pracy naszej klasy widzimy, że zmienna ThreadLocal pozostaje taka sama niezależnie od wątku, który uzyskuje do niej dostęp, ale liczba wątków rośnie.

Licznik: 1
Licznik: 2
Licznik: 3
wątekLocalCounter: 0
wątekLocalCounter: 0
wątekLocalCounter: 0

Proces zakończony kodem wyjścia 0

usunąć()

Aby zrozumieć, jak działa metoda usuwania , zmienimy nieco kod klasy ThreadDemo :


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

W tym kodzie ustalimy, że jeśli ten licznik wątków jest liczbą parzystą, wywołamy metodę remove() na naszej zmiennej ThreadLocal . Wynik kodu:

Licznik: 3
wątki Licznik lokalny: 0
Licznik: 2
wątki Licznik lokalny: null
Licznik: 1
wątek Licznik lokalny: 0

Proces zakończony kodem wyjścia 0

I tutaj możemy łatwo zobaczyć, że zmienna ThreadLocal w drugim wątku ma wartość null .

ThreadLocal.withInitial()

Ta metoda tworzy zmienną lokalną wątku.

Implementacja klasy 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());
    }
}

I możemy spojrzeć na wynik naszego kodu:

Licznik: 1
Licznik: 2
Licznik: 3
wątki Licznik lokalny: 1
wątek Licznik lokalny: 1
wątek Licznik lokalny: 1

Proces zakończony kodem wyjścia 0

Dlaczego powinniśmy używać takich zmiennych?

ThreadLocal zapewnia abstrakcję nad zmiennymi lokalnymi w odniesieniu do wątku wykonania java.lang.Thread .

Zmienne ThreadLocal różnią się od zwykłych tym, że każdy wątek ma swoją własną, indywidualnie inicjowaną instancję zmiennej, do której uzyskuje dostęp za pomocą metod get() lub set() .

Z każdym wątkiem, czyli instancją klasy Thread , jest powiązana tabela zmiennych ThreadLocal . Klucze tabeli to odniesienia do obiektów klasy ThreadLocal , a wartości to odniesienia do obiektów „przechwyconych” przez zmienne ThreadLocal.

Dlaczego generowanie liczb losowych przez Random nie jest odpowiednie dla aplikacji wielowątkowych?

Używamy klasy Random , aby uzyskać losową liczbę. Ale czy nadal będzie działać równie dobrze w środowisku wielowątkowym? Nie bardzo. Random nie nadaje się do wielowątkowości, ponieważ gdy wiele wątków uzyskuje dostęp do klasy w tym samym czasie, staje się ona mniej wydajna.

W tym celu JDK 7 wprowadził klasę java.util.concurrent.ThreadLocalRandom do generowania liczb losowych w środowisku wielowątkowym. Składa się z dwóch klas: ThreadLocal i Random .

Losowa liczba otrzymana przez jeden wątek jest niezależna od innego wątku, podczas gdy java.util.Random zapewnia globalne liczby losowe. Ponadto, w przeciwieństwie do Random , ThreadLocalRandom nie obsługuje jawnego inicjowania. Zamiast tego zastępuje metodę setSeed() odziedziczoną z Random , aby zawsze zgłaszać wyjątek UnsupportedOperationException po wywołaniu.

Rzućmy okiem na metody ThreadLocalRandom :

metoda działanie
ThreadLocalRandom current() Zwraca ThreadLocalRandom bieżącego wątku.
int następny (int bity) Generuje następną liczbę pseudolosową.
double nextDouble(podwójne minimum, podwójna granica) Zwraca pseudolosową, równomiernie rozłożoną wartość między podaną najmniejszą ( najmniejszą ) wartością (włącznie) a granicą ( bound ) (wyłącznie).
int nextInt(int najmniej, int granica) Zwraca pseudolosową, równomiernie rozłożoną wartość między określoną najmniejszą wartością (włącznie) a powiązaną wartością (wyłącznie).
długi następny długi (długie n) Zwraca pseudolosową, równomiernie rozłożoną wartość z przedziału od 0 (włącznie) do podanej wartości (wyłącznie).
long nextLong(najdłuższa najmniej, długa granica) Zwraca pseudolosową, równomiernie rozłożoną wartość między określoną najmniejszą wartością (włącznie) a powiązaną wartością (wyłącznie).
void setSeed(długie nasiona) Zgłasza UnsupportedOperationException . Ustawienie początkowe nie jest obsługiwane w tym generatorze.

Pobieranie losowych danych za pomocą ThreadLocalRandom.current()

ThreadLocalRandom to połączenie klas ThreadLocal i Random . W ten sposób osiąga lepszą wydajność w środowisku wielowątkowym, po prostu unikając współbieżnego dostępu do instancji Random .

Zaimplementujmy przykład dla wielu wątków i zobaczmy, jaki będzie wynik naszej aplikacji z klasą 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));
    }
}

Wynik naszego programu:

Czas potrzebny: 1
Wygenerowano wątek 17 13
Wygenerowano wątek 18 41
Wygenerowano wątek 16 99
Wygenerowano wątek 19 25
Wygenerowano wątek 23
33 Wygenerowano wątek 24 21
Wygenerowano wątek 15 15 Wygenerowano
wątek 21 28 Wygenerowano
wątek 22 97
Wygenerowano wątek 20 33

A teraz zmieńmy naszą klasę RandomNumbers i użyjmy w niej Random :


int result = new Random().nextInt(bound);
Czas potrzebny: 5
Wygenerowano wątek 20 48
Wygenerowano wątek 19 57
Wygenerowano wątek 18 90
Wygenerowano wątek 22 43
Wygenerowano wątek 24 7 Wygenerowano wątek
23 63
Wygenerowano wątek 15 2
Wygenerowano wątek 16 40
Wygenerowano wątek 17 29
Wygenerowano wątek 21 12

Ważne jest, aby pamiętać! W obecnych testach wyniki czasami pokrywały się i miały różną wartość. Ale jeśli mówimy o większej liczbie wątków (na przykład 100), wynik będzie wyglądał następująco:

Losowo — 19-25 ml
ThreadLocalLosowo — 17-19 ml

W związku z tym im więcej wątków w naszej aplikacji, tym silniejsze użycie klasy Random do wielowątkowości obniży wydajność.

Podsumowując i powtarzając różnice między klasą Random a ThreadLocalRandom :

Losowy WątekLokalnyLosowy
Jeśli różne wątki używają tego samego wystąpienia Random , prowadzi to do konfliktów i negatywnie wpływa na wydajność. Nie ma sporów i problemów, ponieważ generowane liczby losowe są lokalne dla bieżącego wątku.
Wykorzystuje liniowy wzór kongruencji do zmiany wartości początkowej. Generator liczb losowych jest inicjowany przy użyciu wewnętrznie generowanego materiału siewnego.
Przydatne w aplikacjach, w których każdy wątek ma swój własny zestaw losowych instancji do użycia. Przydatne w aplikacjach, w których wiele wątków używa liczb losowych równolegle w pulach wątków.
To jest klasa nadrzędna. To jest klasa dziecięca.