Anleitung zu ThreadLocalRandom in Java

1. Übersicht

Das Generieren von Zufallswerten ist eine sehr häufige Aufgabe. Aus diesem Grund stellt Java die Klasse java.util.Random bereit .

Diese Klasse funktioniert jedoch in einer Umgebung mit mehreren Threads nicht gut.

Auf vereinfachte Weise ist der Grund für die schlechte Leistung von Random in einer Umgebung mit mehreren Threads auf Konflikte zurückzuführen, da mehrere Threads dieselbe zufällige Instanz verwenden.

Um diese Einschränkung zu beheben, hat Java in JDK 7 die Klasse java.util.concurrent.ThreadLocalRandom eingeführt, um Zufallszahlen in einer Umgebung mit mehreren Threads zu generieren .

Lassen Sie uns sehen, wie ThreadLocalRandom funktioniert und wie es in realen Anwendungen verwendet wird.

2. ThreadLocalRandom Over Random

ThreadLocalRandom ist eine Kombination aus den Klassen ThreadLocal und Random (dazu später mehr) und ist vom aktuellen Thread isoliert. Auf diese Weise wird eine bessere Leistung in einer Multithread-Umgebung erzielt, indem einfach der gleichzeitige Zugriff auf zufällige Instanzen vermieden wird.

Die von einem Thread erhaltene Zufallszahl wird vom anderen Thread nicht beeinflusst, während java.util.Random global Zufallszahlen bereitstellt.

Im Gegensatz zu Random unterstützt ThreadLocalRandom das explizite Festlegen des Startwerts nicht. Stattdessen überschreibt er die setSeed (lange Samen) Methode geerbt Zufalls immer einen werfen UnsupportedOperationException wenn genannt.

2.1. Thread-Konflikt

Bisher haben wir festgestellt, dass die Random- Klasse in Umgebungen mit hoher Gleichzeitigkeit eine schlechte Leistung erbringt. Um dies besser zu verstehen, sehen wir uns an, wie eine der primären Operationen next (int) implementiert wird:

private final AtomicLong seed; protected int next(int bits) { long oldseed, nextseed; AtomicLong seed = this.seed; do { oldseed = seed.get(); nextseed = (oldseed * multiplier + addend) & mask; } while (!seed.compareAndSet(oldseed, nextseed)); return (int)(nextseed >>> (48 - bits)); }

Dies ist eine Java-Implementierung für den Algorithmus des linearen kongruenten Generators. Es ist offensichtlich, dass alle Threads dieselbe Startinstanzvariable verwenden .

Um den nächsten zufälligen Satz von Bits zu erzeugen, versucht es zuerst den gemeinsamen zu ändern Samt Wert atomar über compareAndSet oder CAS kurz.

Wenn mehrere Threads versuchen, den Startwert gleichzeitig mit CAS zu aktualisieren , gewinnt und aktualisiert ein Thread den Startwert und der Rest verliert. Wenn Threads verloren gehen, wird derselbe Vorgang immer wieder versucht, bis sie die Möglichkeit haben, den Wert zu aktualisieren und letztendlich die Zufallszahl zu generieren.

Dieser Algorithmus ist sperrenfrei und verschiedene Threads können gleichzeitig fortschreiten. Allerdings , wenn die Behauptung hoch ist, wird die Anzahl der CAS Ausfälle und Wiederholungen verletzt die signifikant die Gesamtleistung.

Andererseits entfernt das ThreadLocalRandom diese Konkurrenz vollständig, da jeder Thread seine eigene Instanz von Random und folglich seinen eigenen begrenzten Startwert hat.

Schauen wir uns nun einige Möglichkeiten an, um zufällige int-, long- und double- Werte zu generieren .

3. Generieren von Zufallswerten mit ThreadLocalRandom

Gemäß der Oracle-Dokumentation müssen wir nur die ThreadLocalRandom.current () -Methode aufrufen , und die Instanz von ThreadLocalRandom für den aktuellen Thread wird zurückgegeben . Wir können dann zufällige Werte generieren, indem wir verfügbare Instanzmethoden der Klasse aufrufen.

Lassen Sie uns einen zufälligen int- Wert ohne Grenzen generieren :

int unboundedRandomValue = ThreadLocalRandom.current().nextInt());

Als nächstes wollen wir sehen, wie wir einen zufällig begrenzten int- Wert erzeugen können , dh einen Wert zwischen einer bestimmten unteren und oberen Grenze.

Hier ist ein Beispiel für die Erzeugung eines zufälligen int- Werts zwischen 0 und 100:

int boundedRandomValue = ThreadLocalRandom.current().nextInt(0, 100);

Bitte beachten Sie, dass 0 die inklusive Untergrenze und 100 die ausschließliche Obergrenze ist.

Wir können zufällige Werte für long und double generieren, indem wir die Methoden nextLong () und nextDouble () auf ähnliche Weise wie in den obigen Beispielen aufrufen .

Java 8 fügt auch die nextGaussian () -Methode hinzu, um den nächsten normalverteilten Wert mit einem Mittelwert von 0,0 und einer Standardabweichung von 1,0 von der Generatorsequenz zu generieren.

Wie bei der Random- Klasse können wir auch die Methoden doubles (), ints () und longs () verwenden, um Ströme von Zufallswerten zu generieren.

4. Vergleichen von ThreadLocalRandom und Random mit JMH

Lassen Sie uns sehen, wie wir mithilfe der beiden Klassen zufällige Werte in einer Umgebung mit mehreren Threads generieren und dann deren Leistung mit JMH vergleichen können.

Lassen Sie uns zunächst ein Beispiel erstellen, in dem alle Threads eine einzelne Instanz von Random gemeinsam nutzen. Hier senden wir die Aufgabe, einen Zufallswert mithilfe der Zufallsinstanz zu generieren, an einen ExecutorService:

ExecutorService executor = Executors.newWorkStealingPool(); List
    
      callables = new ArrayList(); Random random = new Random(); for (int i = 0; i { return random.nextInt(); }); } executor.invokeAll(callables);
    

Lassen Sie uns die Leistung des obigen Codes mithilfe des JMH-Benchmarking überprüfen:

# Run complete. Total time: 00:00:36 Benchmark Mode Cnt Score Error Units ThreadLocalRandomBenchMarker.randomValuesUsingRandom avgt 20 771.613 ± 222.220 us/op

In ähnlicher Weise verwenden wir jetzt ThreadLocalRandom anstelle der Random- Instanz, die für jeden Thread im Pool eine Instanz von ThreadLocalRandom verwendet :

ExecutorService executor = Executors.newWorkStealingPool(); List
    
      callables = new ArrayList(); for (int i = 0; i { return ThreadLocalRandom.current().nextInt(); }); } executor.invokeAll(callables);
    

Hier ist das Ergebnis der Verwendung von ThreadLocalRandom:

# Run complete. Total time: 00:00:36 Benchmark Mode Cnt Score Error Units ThreadLocalRandomBenchMarker.randomValuesUsingThreadLocalRandom avgt 20 624.911 ± 113.268 us/op

Finally, by comparing the JMH results above for both Random and ThreadLocalRandom, we can clearly see that the average time taken to generate 1000 random values using Random is 772 microseconds, whereas using ThreadLocalRandom it's around 625 microseconds.

Thus, we can conclude that ThreadLocalRandom is more efficient in a highly concurrent environment.

To learn more about JMH, check out our previous article here.

5. Implementation Details

It's a good mental model to think of a ThreadLocalRandom as a combination of ThreadLocal and Random classes. As a matter of fact, this mental model was aligned with the actual implementation before Java 8.

As of Java 8, however, this alignment broke down completely as the ThreadLocalRandom became a singleton. Here's how the current() method looks in Java 8+:

static final ThreadLocalRandom instance = new ThreadLocalRandom(); public static ThreadLocalRandom current() { if (U.getInt(Thread.currentThread(), PROBE) == 0) localInit(); return instance; }

It's true that sharing one global Random instance leads to sub-optimal performance in high contention. However, using one dedicated instance per thread is also overkill.

Instead of a dedicated instance of Random per thread, each thread only needs to maintain its own seed value. As of Java 8, the Thread class itself has been retrofitted to maintain the seed value:

public class Thread implements Runnable { // omitted @jdk.internal.vm.annotation.Contended("tlr") long threadLocalRandomSeed; @jdk.internal.vm.annotation.Contended("tlr") int threadLocalRandomProbe; @jdk.internal.vm.annotation.Contended("tlr") int threadLocalRandomSecondarySeed; }

The threadLocalRandomSeed variable is responsible for maintaining the current seed value for ThreadLocalRandom. Moreover, the secondary seed, threadLocalRandomSecondarySeed, is usually used internally by the likes of ForkJoinPool.

This implementation incorporates a few optimizations to make ThreadLocalRandom even more performant:

  • Avoiding false sharing by using the @Contented annotation, which basically adds enough padding to isolate the contended variables in their own cache lines
  • Using sun.misc.Unsafe to update these three variables instead of using the Reflection API
  • Avoiding extra hashtable lookups associated with the ThreadLocal implementation

6. Conclusion

This article illustrated the difference between java.util.Random and java.util.concurrent.ThreadLocalRandom.

We also saw the advantage of ThreadLocalRandom over Random in a multithreaded environment, as well as performance and how we can generate random values using the class.

ThreadLocalRandom ist eine einfache Ergänzung zum JDK, kann jedoch bei Anwendungen mit hoher gleichzeitiger Anwendung erhebliche Auswirkungen haben.

Und wie immer finden Sie die Implementierung all dieser Beispiele auf GitHub.