JavaRush /Java блог /Random UA /Теорія ймовірностей на практиці чи знаєте ви про Random
Viacheslav
3 рівень

Теорія ймовірностей на практиці чи знаєте ви про Random

Стаття з групи Random UA
Теорія ймовірностей на практиці чи знаєте ви про 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 . Тобто, клас спроектований так, щоб максимально ефективно підбирати випадкові числа. Однак, як і сказано в документації, вони " не є криптографічно надійними ". Тобто для якихось цілей використання криптографічних цілей (генерація паролів і т.п.) не годиться, т.к. послідовність при належному підході передбачається. На цю тему є в інтернеті приклади, наприклад: " 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 так само змінюється внутрішній Atomic Long . З одного боку, у цьому нічого страшного з логічного погляду, т.к. використовується AtomicLong . З іншого боку, за все треба платити, зокрема продуктивністю. І за це також. Тому навіть в офіційній документації доjava.util.Random сказано: " Instances of java.util.Random are threadsafe. However, current use of the same java.util . . Тобто в багатопотокових додатках при активному використанні 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
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ