Ръководство за ThreadLocalRandom в Java

1. Общ преглед

Генерирането на произволни стойности е много често срещана задача. Ето защо Java предоставя класа java.util.Random .

Този клас обаче не се представя добре в среда с много нишки.

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

За да се справи с това ограничение, Java представи клас java.util.concurrent.ThreadLocalRandom в JDK 7 - за генериране на случайни числа в среда с много нишки .

Нека да видим как се представя ThreadLocalRandom и как да го използваме в реални приложения.

2. ThreadLocalRandom Over Random

ThreadLocalRandom е комбинация от класовете ThreadLocal и Random (повече за това по-късно) и е изолиран от текущата нишка. По този начин той постига по-добра производителност в многонишкова среда, като просто избягва всеки едновременен достъп до екземпляри на Random .

Случайното число, получено от една нишка, не се влияе от другата нишка, докато java.util.Random предоставя случайни числа в световен мащаб.

Също така, за разлика от Random, ThreadLocalRandom не поддържа изрично задаване на семената. Вместо това той отменя метода setSeed (long seed) , наследен от Random, за да изхвърли винаги UnsupportedOperationException, ако е извикан.

2.1. Състезание в нишката

Досега установихме, че класът Random се представя слабо в силно конкурентна среда. За да разберем по-добре това, нека видим как се изпълнява една от основните му операции, next (int) :

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

Това е реализация на Java за алгоритъма на линейния конгруентен генератор. Очевидно е, че всички нишки споделят една и съща променлива на екземпляра на семена .

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

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

Този алгоритъм е без заключване и различни нишки могат да се развиват едновременно. Когато обаче твърдението е високо, броят на CAS неуспехите и повторните опити ще навреди значително на цялостната производителност.

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

Нека сега разгледаме някои от начините за генериране на произволни стойности int, long и double .

3. Генериране на случайни стойности с помощта на ThreadLocalRandom

Според документацията на Oracle, просто трябва да извикаме метода ThreadLocalRandom.current () и той ще върне екземпляра на ThreadLocalRandom за текущата нишка . След това можем да генерираме произволни стойности чрез извикване на налични методи на екземпляр на класа.

Нека генерираме произволна int стойност без никакви граници:

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

След това нека видим как можем да генерираме произволна ограничена int стойност, което означава стойност между дадена долна и горна граница.

Ето пример за генериране на произволна int стойност между 0 и 100:

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

Моля, обърнете внимание, 0 е включителната долна граница, а 100 е изключителната горна граница.

Можем да генерираме произволни стойности за long и double, като извикаме методите nextLong () и nextDouble () по подобен начин, както е показано в примерите по-горе.

Java 8 също добавя метода nextGaussian () , за да генерира следващата нормално разпределена стойност със средно 0,0 и 1,0 стандартно отклонение от последователността на генератора.

Както при класа Random , ние също можем да използваме методите double (), ints () и longs () , за да генерираме потоци от случайни стойности.

4. Сравняване на ThreadLocalRandom и Random с помощта на JMH

Нека да видим как можем да генерираме произволни стойности в многонишкова среда, като използваме двата класа, след което да сравним тяхната производителност с помощта на JMH.

Първо, нека създадем пример, при който всички нишки споделят един екземпляр на Random. Тук изпращаме задачата за генериране на произволна стойност с помощта на случайния екземпляр на ExecutorService:

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

Нека проверим ефективността на кода по-горе, използвайки JMH бенчмаркинг:

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

По същия начин, нека сега използваме ThreadLocalRandom вместо случайния екземпляр, който използва по един екземпляр на ThreadLocalRandom за всяка нишка в пула:

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

Ето резултата от използването на 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 е просто допълнение към JDK, но може да създаде забележимо въздействие в силно едновременни приложения.

И както винаги, изпълнението на всички тези примери може да бъде намерено в GitHub.