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

Генераторы случайных чисел в Java 17

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

1. Обзор

В выпуске Java SE 17 представлено обновление API для генерации случайных чисел — JEP 356 .

С этим обновлением API были введены новые типы интерфейсов, а также методы для простого перечисления, поиска и создания экземпляров фабрик генераторов . Кроме того, теперь доступен новый набор реализаций генератора случайных чисел.

В этом руководстве мы сравним новый API RandomGenerator со старым API Random . Мы рассмотрим перечисление всех доступных фабрик генераторов и выбор генератора на основе его имени или свойства.

Мы также изучим потокобезопасность и производительность нового API.

2. Старый случайный API

Во-первых, давайте взглянем на старый API Java для генерации случайных чисел на основе класса Random .

2.1. Дизайн API

Оригинальный API состоит из четырех классов без интерфейсов:

./b83ad30ea5c3dc4ec0d664ec67407b51.png

2.2. Случайный

Чаще всего используется генератор случайных чисел Random из пакета java.util .

Чтобы сгенерировать поток случайных чисел, нам нужно создать экземпляр класса генератора случайных чисел — Random :

Random random = new Random();
int number = random.nextInt(10);
assertThat(number).isPositive().isLessThan(10);

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

2.3. Альтернативы

В дополнение к java.util.Random доступны три альтернативных генератора для решения проблем безопасности потоков и безопасности .

Все экземпляры Random по умолчанию потокобезопасны. Однако одновременное использование одного и того же экземпляра в нескольких потоках может привести к снижению производительности. Поэтому класс ThreadLocalRandom из пакета java.util.concurrent является предпочтительным вариантом для многопоточных систем.

Поскольку экземпляры Random не являются криптографически безопасными, класс SecureRandom позволяет нам создавать генераторы для использования в контексте, чувствительном к безопасности.

Наконец, класс SplittableRandom из пакета java.util оптимизирован для работы с параллельными потоками и вычислениями в стиле fork/join.

3. Новый API генератора случайных чисел

Теперь давайте взглянем на новый API, основанный на интерфейсе RandomGenerator .

3.1. Дизайн API

Новый API обеспечивает лучший общий дизайн с новыми типами интерфейса и реализациями генератора :

./f40058d5f1ad04248f9b7bd43f2cd412.png

На диаграмме выше мы видим, как старые классы API вписываются в новый дизайн. Помимо этих типов, добавлено несколько классов реализации генератора случайных чисел:

  • Группа Хороширо

  • Xoroshiro128PlusPlus

  • Группа Хосиро

  • Xoshiro256PlusPlus

  • Группа LXM

  • L128X1024СмешанныйСлучайный

  • L128X128MixСлучайный

  • L128X256СмешанныйСлучайный

  • L32X64MixСлучайный

  • L64X1024MixСлучайный

  • L64X128MixСлучайный

  • L64X128ЗвездаЗвездаСлучайный

  • L64X256MixСлучайный

3.2. Области улучшения

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

Например, SplittableRandom был полностью отделен от остального API, хотя некоторые части его кода были полностью идентичны Random .

Поэтому основными целями нового RandomGenerator API являются:

  • Обеспечьте более простую взаимозаменяемость различных алгоритмов
  • Обеспечьте лучшую поддержку потокового программирования
  • Устранение дублирования кода в существующих классах
  • Сохранить существующее поведение старого Random API

3.3. Новые интерфейсы

Новый корневой интерфейс RandomGenerator предоставляет единый API для всех существующих и новых генераторов .

Он определяет методы для возврата случайно выбранных значений разных типов, а также потоков случайно выбранных значений.

Новый API предоставляет дополнительные четыре новых специализированных интерфейса генератора:

  • SplitableGenerator позволяет создать новый генератор как потомка текущего.
  • JumpableGenerator позволяет перескакивать вперед на умеренное количество розыгрышей
  • LeapableGenerator позволяет опережать большое количество розыгрышей
  • ПроизвольноJumpableGenerator добавляет расстояние прыжка к LeapableGenerator

4. Фабрика случайных генераторов

В новом API доступен фабричный класс для генерации нескольких генераторов случайных чисел по определенному алгоритму.

4.1. Найти все

Метод RandomGeneratorFactory создает непустой поток всех доступных фабрик генераторов .

Мы можем использовать его для печати всех зарегистрированных фабрик генераторов и проверки свойств их алгоритма :

RandomGeneratorFactory.all()
.sorted(Comparator.comparing(RandomGeneratorFactory::name))
.forEach(factory -> System.out.println(String.format("%s\t%s\t%s\t%s",
factory.group(),
factory.name(),
factory.isJumpable(),
factory.isSplittable())));

Доступность фабрик определяется путем обнаружения реализаций интерфейса RandomGenerator через API поставщика услуг.

4.2. Найти по свойству

Мы также можем использовать метод all для запроса фабрик по свойствам алгоритма генератора случайных чисел :

RandomGeneratorFactory.all()
.filter(RandomGeneratorFactory::isJumpable)
.findAny()
.map(RandomGeneratorFactory::create)
.orElseThrow(() -> new RuntimeException("Error creating a generator"));

Поэтому с помощью Stream API мы можем найти фабрику, удовлетворяющую нашим требованиям, а затем использовать ее для создания генератора.

5. Выбор случайного генератора

В дополнение к обновленному дизайну API было реализовано несколько новых алгоритмов, и, вероятно, в будущем будет добавлено больше.

5.1. Выберите по умолчанию

В большинстве случаев у нас нет конкретных требований к генератору. Таким образом, мы можем получить генератор по умолчанию непосредственно из интерфейса RandomGenerator .

Это новый рекомендуемый подход в Java 17 в качестве альтернативы созданию экземпляров Random :

RandomGenerator generator = RandomGenerator.getDefault();

В настоящее время метод getDefault выбирает генератор L32X64MixRandom .

Однако со временем алгоритмы могут меняться. Поэтому нет гарантии, что этот метод будет продолжать возвращать этот алгоритм в будущих выпусках.

5.2. Выберите конкретное

С другой стороны, когда у нас есть определенные требования к генератору, мы можем получить конкретный генератор с помощью метода of :

RandomGenerator generator = RandomGenerator.of("L128X256MixRandom");

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

Он выдаст исключение IllegalArgumentException , если указанный алгоритм не будет найден.

6. Безопасность потоков

Большинство новых реализаций генераторов не являются потокобезопасными . Однако и Random , и SecureRandom по- прежнему существуют.

Таким образом, в многопоточных средах мы можем выбрать:

  • Поделиться экземпляром потокобезопасного генератора
  • Разделить новый экземпляр из локального источника перед запуском нового потока

Мы можем достичь второго случая, используя SplittableGenerator :

List<Integer> numbers = Collections.synchronizedList(new ArrayList<>());
ExecutorService executorService = Executors.newCachedThreadPool();

RandomGenerator.SplittableGenerator sourceGenerator = RandomGeneratorFactory
.<RandomGenerator.SplittableGenerator>of("L128X256MixRandom")
.create();

sourceGenerator.splits(20).forEach((splitGenerator) -> {
executorService.submit(() -> {
numbers.add(splitGenerator.nextInt(10));
});
})

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

7. Производительность

Давайте запустим простой тест производительности для всех доступных реализаций генератора в Java 17.

Мы будем тестировать генераторы по одному и тому же методу, генерируя четыре разных типа случайных чисел:

private static void generateRandomNumbers(RandomGenerator generator) {
generator.nextLong();
generator.nextInt();
generator.nextFloat();
generator.nextDouble();
}

Посмотрим на результаты тестов:

   | Алгоритм    | Режим    | Счет    | Ошибка    | Единицы   | 
| L128X1024СмешанныйСлучайный | среднее | 95 637 | ±3274 | нс/оп |
| L128X128MixСлучайный | среднее | 57 899 | ±2162 | нс/оп |
| L128X256СмешанныйСлучайный | среднее | 66 095 | ±3260 | нс/оп |
| L32X64MixСлучайный | среднее | 35 717 | ±1737 | нс/оп |
| L64X1024MixСлучайный | среднее | 73 690 | ±4967 | нс/оп |
| L64X128MixСлучайный | среднее | 35 261 | ±1985 | нс/оп |
| L64X128ЗвездаЗвездаСлучайный | среднее | 34 054 | ±0,314 | нс/оп |
| L64X256MixСлучайный | среднее | 36 238 | ±0,090 | нс/оп |
| Случайный | среднее | 111 369 | ±0,329 | нс/оп |
| SecureRandom | среднее | 9 457 881 | ±45 574 | нс/оп |
| РазделяемыйСлучайный | среднее | 27 753 | ±0,526 | нс/оп |
| Xoroshiro128PlusPlus | среднее | 31 825 | ±1863 | нс/оп |
| Xoshiro256PlusPlus | среднее | 33 327 | ±0,555 | нс/оп |

SecureRandom — самый медленный генератор, но это потому, что это единственный криптографически сильный генератор.

Поскольку им не обязательно быть потокобезопасными, новые реализации генераторов работают быстрее по сравнению с Random .

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

В этой статье мы рассмотрели обновления в API для генерации случайных чисел, новую функцию в Java SE 17 .

Мы узнали о различиях между старым и новым API. Включая представленный новый дизайн API, интерфейсы и реализации.

В примерах мы увидели, как найти подходящий алгоритм генератора с помощью RandomGeneratorFactory . Мы также увидели, как выбрать алгоритм на основе его имени или свойства.

Наконец, мы рассмотрели потокобезопасность и производительность старой и новой реализации генератора.

Как всегда, полный исходный код доступен на GitHub .