In dieser Lektion sprechen wir allgemein über die Arbeit mit der Klasse java.lang.ThreadLocal<> und deren Verwendung in einer Multithread-Umgebung.

Die ThreadLocal- Klasse wird zum Speichern von Variablen verwendet. Ein besonderes Merkmal dieser Klasse besteht darin, dass sie für jeden Thread, der sie verwendet, eine separate, unabhängige Kopie eines Werts speichert.

Wenn wir tiefer in die Funktionsweise der Klasse eintauchen, können wir uns eine Map vorstellen , die Threads Werten zuordnet, von denen der aktuelle Thread den entsprechenden Wert übernimmt, wenn er ihn verwenden muss.

Konstruktor der ThreadLocal-Klasse

Konstrukteur Aktion
ThreadLocal() Erstellt eine leere Variable in Java

Methoden

Methode Aktion
erhalten() Gibt den Wert der lokalen Variablen des aktuellen Threads zurück
Satz() Legt den Wert der lokalen Variablen für den aktuellen Thread fest
entfernen() Entfernt den Wert der lokalen Variablen des aktuellen Threads
ThreadLocal.withInitial() Zusätzliche Factory-Methode, die den Anfangswert festlegt

get() & set()

Schreiben wir ein Beispiel, in dem wir zwei Zähler erstellen. Die erste, eine gewöhnliche Variable, dient zum Zählen der Anzahl der Threads. Im zweiten Fall werden wir einen ThreadLocal einbinden . Und wir werden sehen, wie sie zusammenarbeiten. Schreiben wir zunächst eine ThreadDemo- Klasse, die Runnable erbt und unsere Daten sowie die überaus wichtige run()- Methode enthält. Wir werden auch eine Methode zum Anzeigen der Zähler auf dem Bildschirm hinzufügen:


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

Mit jedem Durchlauf unserer Klasse erhöhen wir dieSchalterVariable rufen Sie die Methode get() auf, um Daten aus der ThreadLocal- Variablen abzurufen . Wenn der neue Thread keine Daten hat, setzen wir ihn auf 0. Wenn Daten vorhanden sind, erhöhen wir ihn um eins. Und schreiben wir unsere Hauptmethode :


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

}

Beim Ausführen unserer Klasse sehen wir, dass die ThreadLocal- Variable unabhängig vom Thread, der darauf zugreift, gleich bleibt, die Anzahl der Threads jedoch zunimmt.

Zähler: 1
Zähler: 2
Zähler: 3
threadLocalCounter: 0
threadLocalCounter: 0
threadLocalCounter: 0

Prozess mit Exit-Code 0 beendet

entfernen()

Um zu verstehen, wie die Remove- Methode funktioniert, ändern wir den Code in der ThreadDemo- Klasse nur geringfügig:


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

Wenn in diesem Code der Thread-Zähler eine gerade Zahl ist, rufen wir die Methode „remove()“ für unsere ThreadLocal- Variable auf. Ergebnis:

Zähler: 3
threadLocalCounter: 0
Zähler: 2
threadLocalCounter: null
Zähler: 1
threadLocalCounter: 0

Prozess mit Exit-Code 0 beendet

Und hier können wir leicht erkennen, dass die ThreadLocal- Variable im zweiten Thread null ist .

ThreadLocal.withInitial()

Diese Methode erstellt eine Thread-lokale Variable.

Implementierung der ThreadDemo- Klasse:


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

Und wir können uns das Ergebnis unseres Codes ansehen:

Zähler: 1
Zähler: 2
Zähler: 3
threadLocalCounter: 1
threadLocalCounter: 1
threadLocalCounter: 1

Prozess mit Exit-Code 0 beendet

Warum sollten wir solche Variablen verwenden?

ThreadLocal bietet eine Abstraktion über lokale Variablen in Bezug auf den Ausführungsthread java.lang.Thread .

ThreadLocal- Variablen unterscheiden sich von gewöhnlichen Variablen darin, dass jeder Thread über eine eigene, individuell initialisierte Instanz der Variablen verfügt, auf die über dieMethoden get() und set() zugegriffen wird.

Jedem Thread, also jeder Instanz der Thread- Klasse, ist eine Zuordnung von ThreadLocal- Variablen zugeordnet. Die Schlüssel der Karte sind Verweise auf ThreadLocal- Objekte und die Werte sind Verweise auf „erworbene“ ThreadLocal -Variablen.

Warum ist die Random-Klasse nicht für die Generierung von Zufallszahlen in Multithread-Anwendungen geeignet?

Wir verwenden die Random- Klasse, um Zufallszahlen zu erhalten. Aber funktioniert es in einer Multithread-Umgebung genauso gut? Nicht wirklich. Random eignet sich nicht für Multithread-Umgebungen, da die Leistung leidet, wenn mehrere Threads gleichzeitig auf eine Klasse zugreifen.

Um dieses Problem zu lösen, führte JDK 7 die Klasse java.util.concurrent.ThreadLocalRandom ein , um Zufallszahlen in einer Multithread-Umgebung zu generieren. Es besteht aus zwei Klassen: ThreadLocal und Random .

Die von einem Thread empfangenen Zufallszahlen sind unabhängig von anderen Threads, aber java.util.Random stellt global Zufallszahlen bereit. Außerdem unterstützt ThreadLocalRandom im Gegensatz zu Random kein explizites Seeding. Stattdessen überschreibt es die von Random geerbte setSeed()- Methode , sodass beim Aufruf immer eine UnsupportedOperationException ausgelöst wird.

Schauen wir uns die Methoden der ThreadLocalRandom- Klasse an:

Methode Aktion
ThreadLocalRandom current() Gibt den ThreadLocalRandom des aktuellen Threads zurück.
int next(int bits) Erzeugt die nächste Pseudozufallszahl.
double nextDouble(double least, doublebound) Gibt eine Pseudozufallszahl aus einer gleichmäßigen Verteilung zwischen der kleinsten (einschließlich) und der gebundenen (exklusiven) Zahl zurück.
int nextInt(mindestens int gebunden) Gibt eine Pseudozufallszahl aus einer gleichmäßigen Verteilung zwischen der kleinsten (einschließlich) und der gebundenen (exklusiven) Zahl zurück.
long nextLong(long n) Gibt eine Pseudozufallszahl aus einer gleichmäßigen Verteilung zwischen 0 (einschließlich) und dem angegebenen Wert (ausschließlich) zurück.
long nextLong(long least, longbound) Gibt eine Pseudozufallszahl aus einer gleichmäßigen Verteilung zwischen der kleinsten (einschließlich) und der gebundenen (exklusiven) Zahl zurück.
void setSeed (langer Seed) Löst UnsupportedOperationException aus . Dieser Generator unterstützt kein Seeding.

Zufallszahlen mit ThreadLocalRandom.current() abrufen

ThreadLocalRandom ist eine Kombination aus den Klassen ThreadLocal und Random . Es erreicht eine bessere Leistung in einer Multithread-Umgebung, indem es einfach jeglichen gleichzeitigen Zugriff auf Instanzen der Random- Klasse vermeidet.

Lassen Sie uns ein Beispiel mit mehreren Threads implementieren und sehen, wie unsere Anwendung mit der ThreadLocalRandom- Klasse funktioniert:


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

Ergebnis unseres Programms:

Benötigte Zeit: 1
Thread 17 generiert 13
Thread 18 generiert 41
Thread 16 generiert 99
Thread 19 generiert 25
Thread 23 generiert 33
Thread 24 generiert 21
Thread 15 generiert 15
Thread 21 generiert 28
Thread 22 generiert 97
Thread 20 generiert 33

Und jetzt ändern wir unsere RandomNumbers- Klasse und verwenden darin Random :


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

Beachten! In unseren Tests waren die Ergebnisse manchmal gleich und manchmal unterschiedlich. Wenn wir jedoch mehr Threads verwenden (z. B. 100), sieht das Ergebnis folgendermaßen aus:

Zufällig – 19–25 ms
ThreadLocalRandom – 17–19 ms

Je mehr Threads in unserer Anwendung vorhanden sind, desto größer ist der Leistungseinbruch bei Verwendung der Random- Klasse in einer Multithread-Umgebung.

Um die Unterschiede zwischen den Klassen Random und ThreadLocalRandom zusammenzufassen und zu wiederholen :

Willkürlich ThreadLocalRandom
Wenn verschiedene Threads dieselbe Instanz von Random verwenden , kommt es zu Konflikten und die Leistung wird beeinträchtigt. Es gibt keine Konflikte oder Probleme, da die generierten Zufallszahlen lokal für den aktuellen Thread sind.
Verwendet eine lineare Kongruenzformel, um den Anfangswert zu ändern. Der Zufallszahlengenerator wird mithilfe eines intern generierten Startwerts initialisiert.
Nützlich in Anwendungen, in denen jeder Thread seinen eigenen Satz zufälliger Objekte verwendet. Nützlich in Anwendungen, in denen mehrere Threads parallel Zufallszahlen in Thread-Pools verwenden.
Dies ist eine übergeordnete Klasse. Dies ist eine Kinderklasse.