1. Обзор
Генерация случайных значений — очень распространенная задача. Вот почему Java предоставляет класс java.util.Random .
Однако этот класс плохо работает в многопоточной среде.
Проще говоря, причина низкой производительности Random
в многопоточной среде связана с конкуренцией, учитывая, что несколько потоков совместно используют один и тот же экземпляр Random .
Чтобы обойти это ограничение, Java представила класс java.util.concurrent.ThreadLocalRandom
в JDK 7 — для генерации случайных чисел в многопоточной среде .
Давайте посмотрим, как работает ThreadLocalRandom
и как его использовать в реальных приложениях.
2. ThreadLocalRandom
вместо Random
ThreadLocalRandom
представляет собой комбинацию классов ThreadLocal
и Random
(подробнее об этом позже) и изолирован от текущего потока. Таким образом, он достигает лучшей производительности в многопоточной среде, просто избегая любого параллельного доступа к экземплярам Random
.
Случайное число, полученное одним потоком, не зависит от другого потока, тогда как java.util.Random
предоставляет случайные числа глобально.
Кроме того, в отличие от 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 для алгоритма Linear Congruential Generator . Очевидно, что все потоки используют одну и ту же переменную начального экземпляра.
Чтобы сгенерировать следующий случайный набор битов, он сначала пытается атомарно изменить общее начальное
значение с помощью compareAndSet
или , для краткости, CAS
.
Когда несколько потоков пытаются одновременно обновить начальное
число с помощью CAS, выигрывает один поток и обновляет начальное значение,
а остальные проигрывают. Проигравшие потоки будут повторять один и тот же процесс снова и снова, пока не получат возможность обновить значение и , в конечном итоге, сгенерировать случайное число.
Этот алгоритм не блокируется, и разные потоки могут выполняться одновременно. Однако, когда конкуренция высока, количество сбоев и повторных попыток CAS значительно повлияет на общую производительность.
С другой стороны, ThreadLocalRandom
полностью устраняет это соперничество, поскольку каждый поток имеет свой собственный экземпляр Random
и, следовательно, свое собственное ограниченное семя.
Давайте теперь рассмотрим некоторые способы генерации случайных значений int, long
и double .
3. Генерация случайных значений с помощью ThreadLocalRandom
Согласно документации Oracle, нам просто нужно вызвать метод ThreadLocalRandom.current()
, и он вернет экземпляр ThreadLocalRandom
для текущего потока . Затем мы можем генерировать случайные значения, вызывая доступные методы экземпляра класса.
Давайте сгенерируем случайное значение int
без каких-либо ограничений:
int unboundedRandomValue = ThreadLocalRandom.current().nextInt());
Далее давайте посмотрим, как мы можем сгенерировать случайное ограниченное целочисленное
значение, то есть значение между заданным нижним и верхним пределом.
Вот пример генерации случайного значения int
от 0 до 100:
int boundedRandomValue = ThreadLocalRandom.current().nextInt(0, 100);
Обратите внимание, что 0 – это нижний предел с включением, а 100 – исключительный верхний предел.
Мы можем генерировать случайные значения для long
и double
, вызывая методы nextLong()
и nextDouble()
аналогично тому, как показано в примерах выше.
Java 8 также добавляет метод nextGaussian()
для генерации следующего нормально распределенного значения со средним значением 0,0 и стандартным отклонением 1,0 от последовательности генератора.
Как и в случае с классом Random
, мы также можем использовать методы doubles(), ints()
и longs()
для генерации потоков случайных значений.
4. Сравнение ThreadLocalRandom
и Random
с использованием JMH
Давайте посмотрим, как мы можем генерировать случайные значения в многопоточной среде, используя два класса, а затем сравним их производительность с помощью JMH.
Во-первых, давайте создадим пример, в котором все потоки совместно используют один экземпляр Random.
Здесь мы передаем задачу генерации случайного значения с использованием экземпляра Random в
ExecutorService:
ExecutorService executor = Executors.newWorkStealingPool();
List<Callable<Integer>> 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<Callable<Integer>> 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
как о комбинации классов ThreadLocal
и Random .
На самом деле эта ментальная модель была согласована с фактической реализацией до Java 8.
Однако в Java 8 это выравнивание полностью сломалось, поскольку ThreadLocalRandom
стал singleton . Вот как метод current()
выглядит в Java 8+:
static final ThreadLocalRandom instance = new ThreadLocalRandom();
public static ThreadLocalRandom current() {
if (U.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
Это правда, что совместное использование одного глобального экземпляра Random
приводит к неоптимальной производительности в условиях высокой конкуренции. Однако использование одного выделенного экземпляра на поток также является излишним.
Вместо выделенного экземпляра Random
для каждого потока каждый поток должен поддерживать только свое собственное начальное
значение . Начиная с Java 8, сам класс Thread
был модифицирован для сохранения начального
значения:
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;
}
Переменная threadLocalRandomSeed
отвечает за сохранение текущего начального значения для ThreadLocalRandom.
Более того, вторичное семя threadLocalRandomSecondarySeed
обычно используется внутри такими организациями, как ForkJoinPool.
Эта реализация включает в себя несколько оптимизаций, чтобы сделать ThreadLocalRandom
еще более производительным:
- Предотвращение ложного совместного использования с помощью аннотации
@Contented
, которая в основном добавляет достаточно заполнения, чтобы изолировать соперничающие переменные в их собственных строках кэша. - Использование
sun.misc.Unsafe
для обновления этих трех переменных вместо использования Reflection API - Избегание дополнительных операций поиска в хеш-таблицах, связанных с реализацией
ThreadLocal .
6. Заключение
В этой статье показано различие между java.util.Random
и java.util.concurrent.ThreadLocalRandom
.
Мы также увидели преимущество ThreadLocalRandom
перед Random
в многопоточной среде, а также производительность и то, как мы можем генерировать случайные значения с помощью класса.
ThreadLocalRandom
— это простое дополнение к JDK, но оно может оказать заметное влияние на приложения с высокой степенью параллельности.
И, как всегда, реализацию всех этих примеров можно найти на GitHub .