Рубрики
Без рубрики

Руководство по ThreadLocalRandom на Java

Научитесь генерировать случайные значения в многопоточной среде с помощью ThreadLocalRandom.

Автор оригинала: baeldung.

1. Обзор

Генерация случайных значений-очень распространенная задача. Вот почему Java предоставляет java.util.Случайный класс.

Однако этот класс плохо работает в многопоточной среде.

В упрощенном виде причина низкой производительности Случайный в многопоточной среде это связано с разногласиями, учитывая, что несколько потоков используют один и тот же Случайный пример.

Для устранения этого ограничения, Java представила Java представила класс в JDK 7 – для генерации случайных чисел в многопоточной среде .

Давайте посмотрим, как ThreadLocalRandom выполняет и как его использовать в реальных приложениях.

2. ThreadLocalRandom Над Случайным

ThreadLocalRandom представляет собой комбинацию Резьбонарезной и Случайный классы (подробнее об этом позже) и изолирован от текущего потока. Таким образом, он обеспечивает лучшую производительность в многопоточной среде, просто избегая любого параллельного доступа к экземплярам Случайный .

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

Кроме того, в отличие от Random, ThreadLocalRandom не поддерживает явную настройку начального значения. Вместо этого он переопределяет метод setSeed(длинное начальное значение) , унаследованный от 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 для Линейного конгруэнтного генератора алгоритма. Очевидно, что все потоки используют одну и ту же переменную экземпляра seed .

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

Когда несколько потоков пытаются обновить начальное значение одновременно, используя РЕГИСТР, один поток выигрывает и обновляет начальное значение, , а остальные проигрывают. Проигравшие потоки будут повторять один и тот же процесс снова и снова, пока не получат возможность обновить значение и в конечном итоге сгенерировать случайное число.

Этот алгоритм не блокируется, и различные потоки могут выполняться одновременно. Однако когда конкуренция высока, количество сбоев и повторных попыток CAS значительно снизит общую производительность.

С другой стороны, ThreadLocalRandom полностью устраняет это противоречие, поскольку каждый поток имеет свой собственный экземпляр Random и, следовательно, свое собственное ограниченное семя.

Давайте теперь рассмотрим некоторые из способов генерации случайных int, длинный и двойной ценности.

3. Генерация Случайных Значений С Помощью ThreadLocalRandom

В соответствии с документацией Oracle, нам просто нужно позвонить ThreadLocalRandom.current() метод, и он вернет экземпляр ThreadLocalRandom для текущего потока . Затем мы можем генерировать случайные значения, вызывая доступные методы экземпляра класса.

Давайте сгенерируем случайное int значение без каких-либо ограничений:

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

Далее давайте посмотрим, как мы можем сгенерировать случайное ограниченное инт значение, означающее значение между заданным нижним и верхним пределом.

Вот пример генерации случайного инт значение от 0 до 100:

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

Пожалуйста, обратите внимание, что 0-это включительный нижний предел, а 100-эксклюзивный верхний предел.

Мы можем генерировать случайные значения для long и double , вызывая методы next Long() и nextDouble() аналогично, как показано в примерах выше.

Java 8 также добавляет метод nextGaussian() для генерации следующего нормально распределенного значения со средним значением 0,0 и стандартным отклонением 1,0 от последовательности генератора.

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

4. Сравнение ThreadLocalRandom и Random с использованием JMH

Давайте посмотрим, как мы можем генерировать случайные значения в многопоточной среде, используя два класса, а затем сравнить их производительность с помощью JMH.

Во-первых, давайте создадим пример, в котором все потоки совместно используют один экземпляр Random. Здесь мы отправляем задачу генерации случайного значения с использованием экземпляра Random в службу ExecutorService:

ExecutorService executor = Executors.newWorkStealingPool();
List> callables = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < 1000; i++) {
    callables.add(() -> {
         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 вместо экземпляра Random , который использует один экземпляр ThreadLocalRandom для каждого потока в пуле:

ExecutorService executor = Executors.newWorkStealingPool();
List> callables = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
    callables.add(() -> {
        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

Наконец, сравнивая результаты JMH выше для Random и ThreadLocalRandom , мы можем ясно видеть, что среднее время, необходимое для генерации 1000 случайных значений с использованием Random , составляет 772 микросекунды, тогда как при использовании ThreadLocalRandom оно составляет около 625 микросекунд.

Таким образом, мы можем сделать вывод, что ThreadLocalRandom более эффективен в среде с высокой степенью параллельности .

Чтобы узнать больше о JMH , ознакомьтесь с нашей предыдущей статьей здесь .

5. Детали Реализации

Это хорошая мысленная модель, чтобы думать о ThreadLocalRandom как комбинация локальных и Случайных классов. На самом деле, эта мысленная модель была согласована с реальной реализацией до Java 8.

Однако начиная с Java 8 это выравнивание полностью нарушилось, так как ThreadLocalRandom стал одноэлементным . Вот как выглядит метод current() в Java 8+:

static final ThreadLocalRandom instance = new ThreadLocalRandom();

public static ThreadLocalRandom current() {
    if (U.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();

    return instance;
}

Это правда, что совместное использование одного глобального случайного экземпляра приводит к неоптимальной производительности при высокой конкуренции. Однако использование одного выделенного экземпляра для каждого потока также является излишним.

Вместо выделенного экземпляра Random для каждого потока каждому потоку необходимо поддерживать только свое собственное начальное значение . Начиная с Java 8, сам класс Thread был модифицирован для сохранения значения seed :

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

Переменная ThreadLocal Random Seed отвечает за поддержание текущего начального значения для ThreadLocalRandom. Кроме того, вторичное семя, ThreadLocalRandom Вторичное семя , обычно используется внутри подобных ForkJoinPool.

Эта реализация включает в себя несколько оптимизаций, которые необходимо выполнить ThreadLocalRandom еще более производительный:

  • Избегая ложного совместного использования с помощью аннотации @Contented , которая в основном добавляет достаточное заполнение, чтобы изолировать спорные переменные в их собственных строках кэша
  • Использование sun.разное.Небезопасно для обновления этих трех переменных вместо использования API отражения
  • Избегание дополнительных поисков хэш-таблиц, связанных с реализацией ThreadLocal

6. Заключение

Эта статья проиллюстрировала разницу между Эта статья проиллюстрировала разницу между и и .

Мы также увидели преимущество ThreadLocalRandom над Случайный в многопоточной среде, а также производительность и то, как мы можем генерировать случайные значения с помощью класса.

ThreadLocalRandom это простое дополнение к JDK, но оно может оказать заметное влияние в приложениях с высокой параллельностью.

И, как всегда, реализацию всех этих примеров можно найти на GitHub .