JavaRush /Java Blog /Random-IT /Теория вероятностей на практике или знаете ли вы о Random...
Viacheslav
Livello 3

Теория вероятностей на практике или знаете ли вы о Random

Pubblicato nel gruppo Random-IT
Теория вероятностей на практике or знаете ли вы о Random - 1

Вступление

В мире существует немало наук, которые изучают теорию вероятности. И науки состоят из различных разделов. Например, в математике есть отдельный раздел, посвящённый исследованию случайных событий, величин и т.п. А науки ведь берутся не просто так. В данном случае, теория вероятности начала формироваться, когда люди пытались понять, Howие закономерности есть в бросании кубика при игре в азартные игры. Если приглядеться, вокруг нас есть множество на первый взгляд случайных вещей. Но всё случайное не совсем случайное. Но об этом чуть позже. В языке программирования Java тоже есть поддержка случайных чисел, начиная ещё с первой версии JDK. Случайные числа в Java можно использовать при помощи класса java.util.Random. Для испытаний нам подойдёт tutorialspoint java online compiler. Вот примитивный пример использования Random для эмулирования бросания "костей", or по-русски кубика:

import java.util.Random;

public class HelloWorld{
    public static void main(String []args){
        Random rnd = new Random();
        int number = rnd.nextInt(6) + 1;
        System.out.println("Random number: " + number);
    }
}
Казалось бы, на этом можно закончить описание Random, но не так всё просто. Откроем-ка мы описание класса java.util.Random в Java API. И тут мы видим интересные вещи. Класс Random использует псевдослучайные числа. Как так? Получается, случайные числа не такие случайные?
Теория вероятностей на практике or знаете ли вы о Random - 2

Псевдослучайность java.util.Random

Документация класса java.util.Random говорит, что если экземпляры Random созданы с одинаковым параметром seed и с экземплярами выполнены одинаковые последовательности действий, они возвращают идентичные последовательности чисел. И если мы приглядимся, то увидим, что у Random действительно есть конструктор, который принимает некоторое long meaning в качестве seed:

Random rnd1 = new Random(1L);
Random rnd2 = new Random(1L);
boolean test = rnd1.nextInt(6) == rnd2.nextInt(6);
System.out.println("Test: " + test);
Данный пример вернёт true, т.к. seed обоих экземпляров одинаков. What же делать? Отчасти проблему решает конструктор по умолчанию. Ниже приведён пример содержимого конструктора Random:

public Random() {
	this(seedUniquifier() ^ System.nanoTime());
}
Конструктор по умолчанию использует операцию побитового исключающего OR. И использует для этого long представляющий текущее время и некоторый seed:

private static long seedUniquifier() {
	for (;;) {
		long current = seedUniquifier.get();
		long next = current * 181783497276652981L;
		if (seedUniquifier.compareAndSet(current, next))
			return next;
	}
}
Здесь интересно ещё и то, что каждый вызов метода получения seedUniquifier изменяет meaning seedUniquifier. То есть класс спроектирован так, чтобы максимально эффективно подбирать случайные числа. Однако, How и сказано в documentации, они "are not cryptographically secure". То есть для Howих-то целей использования в криптографических целей (генерация паролей и т.п.) не годится, т.к. последовательность при должном подходе предсказывается. На эту тему есть в интернете примеры, например тут: "Predicting the next Math.random() in Java". Или например исходный code тут: "Vulnerability Weak Crypto". У java.util.Random (генератора случайных чисел) есть некий "шорткат", то есть укороченная version вызова, которая выполняется через Math.random:

public static void main(String []args){
	int random_number = 1 + (int) (Math.random() * 6);
	System.out.println("Value: " + random_number);
}
Но если посмотреть внимательно, внутри сидит всё тот же Random:

public static double random() {
	return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
}
private static final class RandomNumberGeneratorHolder {
	static final Random randomNumberGenerator = new Random();
}
В JavaDoc советуют использовать класс SecureRandom для "cryptographically secure pseudo-random number generator".
Теория вероятностей на практике or знаете ли вы о Random - 3

Безопасный Random Java

Класс SecureRandom является наследником java.util.Random и расположен в пакете java.security. Сравнение этих двух классов можно прочитать в статье "Difference between java.util.Random and java.security.SecureRandom". Чем же так хорош этот SecureRandom? Дело в том, что для него источником случайных чисел является такая магически звучащая штука How "пул энтропии ядра". Это одновременно и плюс, и минус. Про минус этого можно прочитать в статье: "The dangers of java.security.SecureRandom". Если кратко, в Linux есть генератор случайных чисел ядра (RNG). RNG генерирует случайные числа на основе данных из пула энтропии (entropy pool), который наполняется на основе случайных событий в системе, таких How тайминги клавиатуры и дисков, движения мыши, прерывания (interrupts), сетевой трафик. Подробнее про пул энтропии изложено в материале "Случайные числа в Linux(RNG) or How «наполнить» /dev/random и /dev/urandom". На Windows системах используется SHA1PRNG, реализованная в sun.security.provider.SecureRandom. С развитием Java менялся и SecureRandom, о чём для полной картины стоит прочитать в обзоре "Java SecureRandom updates as of April 2016".
Теория вероятностей на практике or знаете ли вы о Random - 4

Многопоточность or будь How Цезарь

Если смотреть code класса Random, то вроде ничто не предвещает беды. Методы не помечены How synchronized. Но есть одно НО: при создании Random конструктором по умолчанию в нескольких потоках мы будем между ними делить один и тот же instance seed, по которому будет создаваться Random. А также при получении нового случайного числа у instance так же меняется внутренний AtomicLong. С одной стороны, в этом нет ничего страшного с логической точки зрения, т.к. используется AtomicLong. С другой стороны, за всё надо платить, в том числе производительностью. И за это тоже. Поэтому даже в официальной documentации к java.util.Random сказано: "Instances of java.util.Random are threadsafe. However, the concurrent use of the same java.util.Random instance across threads may encounter contention and consequent poor performance. Consider instead using ThreadLocalRandom in multithreaded designs". То есть в многопоточных applicationsх при активном использовании Random из нескольких потоков лучше использовать класс ThreadLocalRandom. Его использование немного отличается от обычного Random:

public static void main(String []args){
	int rand = ThreadLocalRandom.current().nextInt(1,7);
	System.out.println("Value: " + rand);
}
Как видим, для него мы не указываем seed. Данный пример описан в официальном tutorial от Oracle: Concurrent Random Numbers. Подробнее про данный класс можно прочитать в обзоре: "Guide to ThreadLocalRandom in Java".
Теория вероятностей на практике or знаете ли вы о Random - 5

StreamAPI и Random

Благодаря выходу Java 8 у нас появилось много новых возможностей. В том числе и Stream API. И изменения коснулись и генерации Random значений. Например, в классе Random появorсь новые методы, которые позволяют получить Stream со случайными значениями типа int, double or long. Например:

import java.util.Random;

public class HelloWorld{
    public static void main(String []args){
        new Random().ints(10, 1, 7).forEach(n -> System.out.println(n));
    }    
}
Также появился новый класс SplittableRandom:

import java.util.SplittableRandom;

public class HelloWorld{
    public static void main(String []args){
        new SplittableRandom().ints(10, 1, 7).forEach(n -> System.out.println(n));
    }  
}
Подробнее про отличие SplittableRandom от остальных классов можно прочитать здесь: "Different ways to create Random numbers in Java".

Заключение

Думаю, стоит сделать вывод. Нужно внимательно читать JavaDoc к используемым классам. За такой простой на первый взгляд вещью How Random стоят нюансы, которые могут сыграть злую шутку. #Viacheslav
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION