Распределение случайных чисел
У меня есть два варианта кода:
Вариант 1
int myFunc() {
return new Random().nextInt();
}
Или:
Вариант 2
private static final Random random = new Random();
int myFunc() {
return random.nextInt();
}
Я понимаю, что option 2 более идиоматично. Я задаюсь вопросом о действительности option 1.
В option 1 я буду использовать только первое число, порожденное данным семенем. В option 2 я выбираю семя и генерирую числа n, используя это семя. IIUC гарантии на случайность находятся на этом случае использования.
Мой вопрос, следовательно, если я назову option 1 много раз есть какие-то гарантии по равномерности распределения продукции?
5 ответов:
Мой реальный вопрос заключается в том, является ли вариант 1 математически корректным.Начнем с варианта 2. Генератор случайных чисел, используемый
java.util.Random, задается в javadoc следующим образом:Класс использует 48-битное семя, которое модифицируется с помощью линейной конгруэнтной формулы. (См. Donald Knuth, The Art of Computer Programming, Volume 2, Section 3.2.1.)И есть более конкретная деталь в явадоках различных методов.
Но суть в том, что что мы используем последовательность, порожденную линейной конгруэнтной формулой, и такие формулы имеют значительную степень автокорреляции ... что может оказаться проблематичным.
Теперь с вариантом 1 вы используете другой экземплярRandomс новым семенем каждый раз и применяете один раунд формулы LC. Таким образом, вы получаете последовательность чисел, которые, вероятно, будут автокоррелированы с семенами. Однако семена генерируются по-разному, в зависимости от Java версия.Java 6 делает это:
public Random() { this(++seedUniquifier + System.nanoTime()); } private static volatile long seedUniquifier = 8682522807148012L;... что совсем не случайно. Если вы создали экземпляры
Randomс постоянным интервалом, семена, вероятно, будут расположены близко друг к другу, и поэтому последовательность случайных чисел, полученная вашим вариантом № 1, может быть автоматически коррелирована.Напротив, Java 7 и 8 делают это:
Последовательность семян, произведенных выше, вероятно, будет гораздо лучшим приближением к (истинной) случайности. Это, вероятно, делает ваш Вариант №1 превосходит вариант №2. Недостатком вашего варианта №1 в Java с 6 по 8 является то, чтоpublic Random() { this(seedUniquifier() ^ System.nanoTime()); } private static long seedUniquifier() { // L'Ecuyer, "Tables of Linear Congruential Generators of // Different Sizes and Good Lattice Structure", 1999 for (;;) { long current = seedUniquifier.get(); long next = current * 181783497276652981L; if (seedUniquifier.compareAndSet(current, next)) return next; } } private static final AtomicLong seedUniquifier = new AtomicLong(8682522807148012L);System.nanoTime(), вероятно, вызов включает системный вызов. Это относительно дорого.
Таким образом, короткий ответ заключается в том, что это конкретная версия Java, которая из Варианта № 1 и варианта № 2 производит более качественные "случайные" числа ... с математической точки зрения.
В обоих случаях распределение чисел будет равномерным на достаточно большом объеме выборки, хотя я не уверен, что это так имеет смысл говорить о вероятностных распределениях, когда процесс детерминирован.
Однако ни один из этих подходов не подходит в качестве генератора случайных чисел "криптостойкости".
Быстрый Код:
// For occasional tasks that just need an average quality random number ExecutorService threadPool = Executors.newCachedThreadPool(); threadPool.execute( () -> { ThreadLocalRandom.current().nextInt(); // Fast and unique! } ); // For SecureRandom, high quality random number final Random r = new SecureRandom(); ExecutorService threadPool = Executors.newCachedThreadPool(); threadPool.execute( () -> { r.nextInt(); // sun.security.provider.NativePRNG uses singleton. Can't dodge contention. } ); // Apache Common Math - Mersenne Twister - decent and non-singleton int cpu = Runtime.getRuntime().availableProcessors(); ExecutorService executor = Executors.newFixedThreadPool( cpu ); Map<Thread, RandomGenerator> random = new WeakHashMap<>( cpu, 1.0f ); executor.execute( ()-> { RandomGenerator r; synchronized ( random ) { // Get or create generator. r = random.get( Thread.currentThread() ); if ( r == null ) random.put( Thread.currentThread(), r = new MersenneTwister() ); } r.nextInt( 1000 ); } );
Пояснение:
- Два
Randomодного и того же семени дадут одинаковые числа.Таким образом, мы сосредоточимся на том, можем ли мы гарантировать различные семена.
В теории,
new Random()в каждой нити не гарантирует различных семян.
- новый случайный заполнена nanoTime и "уникальный" номер.
- число не гарантировано уникальным, потому что его вычисление не является синхронизированный.
- как для nanoTime, он обещает быть "по крайней мере хорошо, как currentTimeMillis"
- currentTimeMillis ничего не гарантирует и может быть довольно грубый .
- в реальной жизни два раза одинаковы только на старых системах linux и Win 98.
На практике
new Random()в каждую нить в основном всегда попадают разные семена.
- создание потока дорого. Мой создает 1 на 50 000 НС. И это не так. медленно .
[18]}50 мкс намного выше общих гранулометрических характеристик нанотайма вплоть до нескольких десятков НС.вычисление уникального числа (1.2) также происходит быстро, поэтому получение одного и того же числа очень редко. используйте исполнители для создания пула потоков, чтобы избежать больших накладных расходов на новые потоки. Zapl предложил
ThreadLocalRandom.current().nextInt(). Великая идея.
- Это так не создать новый
Random, но это также линейный конгруэнтный генератор.- он генерирует новый случайный для каждого потока вызова в качестве семени этого потока.
- он построен, чтобы быть очень быстрым в многопоточности. (См. Примечания ниже.)
- он статически засеян
SecureRandom, которые производят более качественные случайные числа."равномерно распределенная" - это лишь малая часть случайности. тесты .
Randomявляетсянесколько однородным , и его результат может бытьпредсказан с учетом только двух значений.SecureRandomгарантирует , что этого не произойдет. (то есть криптографически сильный)- нет никакого риска столкновения семян, Если вы создадите новый
SecureRandomв каждом потоке.- но в настоящее время его источником является один поток в любом случае, параллельной генерации нет.
- для хорошего ГСЧ, поддерживающего многопоточность, найдите внешнюю справку , например Apache Common's MT .
Примечание: детали реализации выведены из исходного кода Java 8. Будущая версия Java может измениться; например,
ThreadLocalRandomиспользуетsun.misc.Unsafeдля хранения семян, который может быть удален в Java 9, заставляя ThreadLocalRandom найти новый способ работы без разногласий.
- нет.
Нет никаких гарантий относительно свойств распределения чисел, которое будет произведено вариантом 1. Как было ясно показано в других ответах, реализация конструктора для
Однако при использовании варианта 2 можно получить математические гарантии относительно распределения чисел, которые будут получены в ходе одного выполнения программы. С линейным конгруэнтным генератором (алгоритм генерации псевдослучайных чисел, используемыйjava.util.Randomзависит от системного времени. Поэтому, чтобы гарантировать свойства распределения номеров, которые вы получаете с опцией 1, вам нужно будет иметь возможность гарантировать распределение номеров, произведенных вызовами, которые ваша программа делает, чтобы получить системное время на любой платформе, где будет работать программа.java.util.Random) некоторые свойства случайности не так хороши, как с другими алгоритмами, но распределение гарантировано относительно форма. Это не обязательно означает, что Вариант 1 не может служить вашим целям. Это зависит от того, что вы делаете.
Java инициализирует случайное семя с помощью
System.nanoTime()и последовательного счетчика. Это дает некоторую гарантию, что семя будет отличаться для каждого вызова, хотя я бы воздержался от того, чтобы называть его криптографически безопасным.С точки зрения производительности-действительно ли вы ожидаете, что блокировка внутреннего состояния Random в варианте 1 будет иметь более высокую производительность, чем все следующие:
- доступ и увеличение volatile long
- получение тока системное время (, которое довольно дорого )
- динамическое распределение
- Еще один объект для сбора мусора
Мое предложение будет заключаться в том, чтобы сделать бенчмарки вашего реального приложения, чтобы выяснить, но я ожидаю, что Вариант 1 будет самым медленным из всех трех.
По моему опыту, наилучший баланс между хорошим распределением и производительностью достигается с помощью чего-то вроде генератора "Messerne Twister" (см. В Apache Commons) . Еще более причудливое решение см. В разделеthis .
Comments