JavaRush /Java блог /Random /Теория вероятностей на практике или знаете ли вы о Random...
Viacheslav
3 уровень

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

Статья из группы Random
Теория вероятностей на практике или знаете ли вы о Random - 1

Вступление

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

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 использует псевдослучайные числа. Как так? Получается, случайные числа не такие случайные?
Теория вероятностей на практике или знаете ли вы о Random - 2

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

Документация класса java.util.Random говорит, что если экземпляры Random созданы с одинаковым параметром seed и с экземплярами выполнены одинаковые последовательности действий, они возвращают идентичные последовательности чисел. И если мы приглядимся, то увидим, что у Random действительно есть конструктор, который принимает некоторое long значение в качестве 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 обоих экземпляров одинаков. Что же делать? Отчасти проблему решает конструктор по умолчанию. Ниже приведён пример содержимого конструктора 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 изменяет значение seedUniquifier. То есть класс спроектирован так, чтобы максимально эффективно подбирать случайные числа. Однако, как и сказано в документации, они "are not cryptographically secure". То есть для каких-то целей использования в криптографических целей (генерация паролей и т.п.) не годится, т.к. последовательность при должном подходе предсказывается. На эту тему есть в интернете примеры, например тут: "Predicting the next Math.random() in Java". Или например исходный код тут: "Vulnerability Weak Crypto". У java.util.Random (генератора случайных чисел) есть некий "шорткат", то есть укороченная версия вызова, которая выполняется через 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".
Теория вероятностей на практике или знаете ли вы о Random - 3

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

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

Многопоточность или будь как Цезарь

Если смотреть код класса Random, то вроде ничто не предвещает беды. Методы не помечены как synchronized. Но есть одно НО: при создании Random конструктором по умолчанию в нескольких потоках мы будем между ними делить один и тот же instance seed, по которому будет создаваться Random. А также при получении нового случайного числа у instance так же меняется внутренний AtomicLong. С одной стороны, в этом нет ничего страшного с логической точки зрения, т.к. используется AtomicLong. С другой стороны, за всё надо платить, в том числе производительностью. И за это тоже. Поэтому даже в официальной документации к 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". То есть в многопоточных приложениях при активном использовании 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".
Теория вероятностей на практике или знаете ли вы о Random - 5

StreamAPI и Random

Благодаря выходу Java 8 у нас появилось много новых возможностей. В том числе и Stream API. И изменения коснулись и генерации Random значений. Например, в классе Random появились новые методы, которые позволяют получить Stream со случайными значениями типа int, double или 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 к используемым классам. За такой простой на первый взгляд вещью как Random стоят нюансы, которые могут сыграть злую шутку. #Viacheslav
Комментарии (44)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Борис Уровень 19
15 января 2024
Не совсем понял - в первом примере, когда используется конструктор по умолчанию, для создания двух чисел с одинаковыми seed, при их сравнении возвращается true. Но ведь конструктор использует System.nanoTime(), который вызывается последовательно, неужели время создания будет идентично с точностью до наносекунд?
Andrey Уровень 21
14 декабря 2023
Вместо подачи, в потоках используй это, на сайте это - видим, что много чего есть, но расстраивает куда это все, как и зачем. Какие-то эфемерные можно проследить. Хотелось бы увидеть хоть одного следильщика. Ну да ладно. Но, что-то мне подсказывает, что для правильного использования на стороне сервера вся нужная инфа уложится в 2-3 абзаца и как оно там под капотом делает цифры вообще без разницы.
Sergei R. Уровень 23
27 ноября 2023
Побитовый оператор: - Вы удивлены, что мы встретились с вами снова, мистер Андерсон?
Anastasia Уровень 20
9 ноября 2023
Я правильно поняла: для генерации паролей нужно использовать SecureRandom вместо Random, тк последний не обеспечивает полную безопасность?
Ataman Уровень 10
20 октября 2023
я ничего не понял, но вроде все можно взломать. Надеюсь скоро на курсе будут реальные лайфхаки как ломать проги))
Anatoly Уровень 30
6 августа 2023
Про пулы энтропии интересно было
Оля23 Уровень 16
25 июля 2023
Как-то сложновато зашла, возможно позже всё станет на свои места. И согласна - не для новичков
12 июля 2023
похоже статья не для новичков
Rustam Уровень 35 Student
28 мая 2023
Хорошая статья! Про пулы энтропии интересно!
22 мая 2023
Ужасная статья! Вместо того чтобы дать материал структурировано и в необходимом объеме она выглядит как справочник с набором ссылок без вменяемого вывода... Все сноски нужно делать в конце статьи для более развернутого и глубокого погружения в тему. Большинство ссылок для новичков вообще бесполезны, потому как они синтаксис то на этом уровне еще не знают, а им про пул энтропии тут читать приходится. Вместо того чтобы указать на нюансы работы класса простым языком здесь тебя отправляют по ссылкам на другие сайты!!! Тогда проще вообще написать вот ссылка doc.oracle.com - читайте просвещайтесь!!!!