Распределение случайных чисел

у меня есть два варианта кода:

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-разрядное семя, которое изменяется, используя линейную congruential формулу. (См. Дональд Кнут, Искусство программирования, Том 2, раздел 3.2.1.)

и более конкретная деталь в различных методах" документация Javadoc.

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

теперь с опцией 1 вы используете другой Random экземпляр с новым семенем каждый раз и применение одного раунда формулы LC. Таким образом, вы получаете последовательность чисел, которые, вероятно, будут автокоррелированы с семенами. Однако семена образуются в различными способами, в зависимости от версии Java.

Java 6 делает это:

 public Random() { this(++seedUniquifier + System.nanoTime()); }
 private static volatile long seedUniquifier = 8682522807148012L;

... что совсем не случайно. Если вы создали Random экземпляры с постоянным интервалом семена, вероятно, будут расположены близко друг к другу, и поэтому последовательность случайных чисел, производимых вашим вариантом № 1, может быть автоматически коррелирована.

напротив, Java 7 и 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);

последовательность семян, произведенных выше, вероятно, будет намного лучшее приближение к (истинной) случайности. Это, вероятно, делает ваш вариант № 1 лучше варианта №2.

недостатком вашего варианта #1 в Java 6 через 8 является то, что System.nanoTime() вероятно, вызов включает в себя системный вызов. Это относительно дорого.


таким образом, короткий ответ заключается в том, что это конкретная версия Java, которая из опции #1 и опции #2 производит более качественные "случайные" числа ... с математической точки зрения.

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

однако ни один из подходов не подходит в качестве генератора случайных чисел "crypto strength".


Быстрый Код:

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

объяснение:

  1. два Random того же семени даст те же числа.
    1. таким образом, мы сосредоточимся на том, можем ли мы гарантировать различные семена.
  2. в теории, new Random() в каждом потоке не гарантирует, разных семян.

    1. новый случайный заполнена nanoTime и "уникальный" число.
    2. число не гарантируется уникальным, поскольку его расчет не синхронизирован.
    3. что касается nanoTime, он гарантирует, что " по крайней мере, хорошо, как currentTimeMillis"
    4. currentTimeMillis ничего не гарантирует и может быть довольно грубой.
    5. в реальной жизни два раза одинаковы только на старые системы linux и Win 98.
  3. на практике new Random() в каждом потоке в основном всегда получают разные семена.

    1. создание потока-это дорого. Шахта создает 1 на 50 000 НС. И это не медленно.
    2. 50μs путь над зернистостями nanoTime общими до несколько десятков НС.
    3. вычисление уникального номера (1.2) также быстро, поэтому получать такие же число очень редко.
    4. использовать исполнители создать нить бассейн чтобы избежать тяжелых новых потоков над головой.
  4. zapl предложил ThreadLocalRandom.current().nextInt(). Великая идея.

    1. он не создает новый Random, а также линейный конгруэнтный генератор.
    2. он генерирует новый случайный для каждого потока вызовов, поскольку этот поток семя.
    3. оно построен для того чтобы быть очень быстр в multi-потоке. (См. Примечания ниже.)
    4. он статически засеян SecureRandom, которые производят более качественные случайные числа.
  5. "uniformally распространяется" - это только одна небольшая часть случайность тесты.

    1. Random is несколько униформу, и его результат может быть предсказал дали всего две ценности.
    2. SecureRandom гарантии этого не происходит. (т. е. криптографически сильный)
    3. нет никакого риска столкновения семян, Если вы создадите новый SecureRandom в каждом потоке.
    4. но в настоящее время его источник один поток во всяком случае, нет параллельного поколения.
    5. для хорошего RNG, который поддерживает многопоточность, найдите внешняя помощь как Apache Common MT.

Примечание: детали реализации выведены из исходного кода Java 8. Будущая версия Java может измениться; например, ThreadLocalRandom использует sun.misc.Unsafe хранить семена, который могут быть удалены в Java 9 заставляя ThreadLocalRandom найти новый способ работы без разногласий.


нет.

нет никаких гарантий в отношении свойств распределения чисел, которые будут получены с помощью опции 1. Как было ясно в других ответах, реализация конструктора для java.util.Random зависит от системного времени. Таким образом, для того, чтобы сделать гарантию на свойства распределения номеров, которые вы получаете с вариантом 1, вам нужно будет иметь возможность сделать гарантии о распределении номеров, произведенных звонками программа позволяет получить системное время на любой платформе, где программа будет работать.

С вариантом 2, однако, есть математические гарантии, которые могут быть сделаны о распределении чисел, которые будут произведены во время одного выполнения программы. С линейным конгруэнтным генератором (алгоритм генерации псевдослучайных чисел, используемый java.util.Random) некоторые свойства случайности не так хороши, как с другими алгоритмами, но распределение гарантировано быть относительно однородным.

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


Java инициализирует случайное семя с System.nanoTime() и последовательный счетчик. Это дает некоторую гарантию того, что семя будет отличаться для каждого вызова, хотя я бы воздержался от его криптографической защиты.

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

  • доступ и увеличение volatile долго
  • получение текущего системного времени (что довольно дорого)
  • динамическое выделение
  • еще один объект для сбора мусора

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


по моему опыту, лучший баланс между хорошим распределением и производительностью дается с помощью чего-то вроде генератора "Messerne Twister"(см. В разделе Apache Commons) . Для еще более причудливого решения см. этой.