Вступление

В мире существует немало наук, которые изучают теорию вероятности. И науки состоят из различных разделов. Например, в математике есть отдельный раздел, посвящённый исследованию случайных событий, величин и т.п. А науки ведь берутся не просто так. В данном случае, теория вероятности начала формироваться, когда люди пытались понять, какие закономерности есть в бросании кубика при игре в азартные игры. Если приглядеться, вокруг нас есть множество на первый взгляд случайных вещей. Но всё случайное не совсем случайное. Но об этом чуть позже. В языке программирования 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 использует псевдослучайные числа. Как так? Получается, случайные числа не такие случайные?

Псевдослучайность 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

Класс 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, то вроде ничто не предвещает беды. Методы не помечены как 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".

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