Перейти к основному содержимому

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

· 6 мин. чтения

Задача: Наибольшая подстрока палиндром

Для заданной строки s, верните наибольшую подстроку палиндром входящую в s. Подстрока — это непрерывная непустая последовательность символов внутри строки. Стока является палиндромом, если она читается одинаково в обоих направлениях...

ANDROMEDA 42

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 .