Wstęp
Na świecie istnieje wiele nauk zajmujących się teorią prawdopodobieństwa. Nauki składają się z różnych działów. Na przykład w matematyce istnieje osobna sekcja poświęcona badaniu zdarzeń losowych, ilości itp. Ale nauki nie można lekceważyć. W tym przypadku teoria prawdopodobieństwa zaczęła nabierać kształtu, gdy ludzie próbowali zrozumieć, jakie wzorce obowiązują podczas rzucania kostką podczas grania w gry losowe. Jeśli przyjrzysz się uważnie, wokół nas znajduje się wiele pozornie przypadkowych rzeczy. Ale nie wszystko, co losowe, jest całkowicie losowe. Ale o tym później. Język programowania Java obsługuje również liczby losowe, począwszy od pierwszej wersji JDK. Liczb losowych w Javie można używać przy użyciu klasy
java.util.Random . Do testów użyjemy
kompilatora online Java tutorialspoint . Oto prymitywny przykład użycia
Random do emulacji rzucania „kostkami” lub kostkami w języku rosyjskim:
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);
}
}
Wydawać by się mogło, że na tym można zakończyć opis
Random , jednak nie jest to takie proste. Otwórzmy opis klasy
java.util.Random w Java API. I tutaj widzimy ciekawe rzeczy. Klasa
Random wykorzystuje liczby pseudolosowe. Jak to? Okazuje się, że liczby losowe nie są aż tak losowe?
Pseudolosowość java.util.Random
Dokumentacja klasy
java.util.Random mówi, że jeśli instancje
klasy Random zostaną utworzone z tym samym parametrem
źródłowym i na instancjach zostaną wykonane te same sekwencje działań, zwrócą one identyczne sekwencje liczb. A jeśli przyjrzymy się uważnie, zobaczymy, że
Random faktycznie ma
konstruktora , który jako zarodek przyjmuje jakąś
długą wartość :
Random rnd1 = new Random(1L);
Random rnd2 = new Random(1L);
boolean test = rnd1.nextInt(6) == rnd2.nextInt(6);
System.out.println("Test: " + test);
Ten przykład zwróci
wartość true , ponieważ
materiał siewny obu instancji jest taki sam. Co robić? Domyślny konstruktor częściowo rozwiązuje problem. Poniżej znajduje się przykład zawartości konstruktora
Random :
public Random() {
this(seedUniquifier() ^ System.nanoTime());
}
Konstruktor domyślny używa bitowej operacji
OR na wyłączność .
I używa long reprezentującego bieżący czas i trochę
materiału siewnego do tego :
private static long seedUniquifier() {
for (;;) {
long current = seedUniquifier.get();
long next = current * 181783497276652981L;
if (seedUniquifier.compareAndSet(current, next))
return next;
}
}
Kolejną interesującą rzeczą jest to, że każde wywołanie metody pobierającej
nasionUniquifier zmienia wartość
metody nasionUniquifier . Oznacza to, że klasa ma na celu możliwie najskuteczniejsze wybieranie liczb losowych. Jednak, jak mówi dokumentacja, „
nie są one bezpieczne kryptograficznie ”. Oznacza to, że do niektórych zastosowań w celach kryptograficznych (generowanie haseł itp.) nie jest odpowiedni, ponieważ przewidywana jest sekwencja z właściwym podejściem. Przykłady na ten temat można znaleźć w Internecie, na przykład tutaj: „
Przewidywanie następnego Math.random() w Javie ”. Lub na przykład kod źródłowy tutaj: „
Vulnerability Weak Crypto ”. Java.util.Random (generator liczb losowych) ma pewien „skrót”
, czyli skróconą wersję wywołania, które jest wykonywane poprzez Math.random:
public static void main(String []args){
int random_number = 1 + (int) (Math.random() * 6);
System.out.println("Value: " + random_number);
}
Ale jeśli przyjrzysz się uważnie, w środku znajduje się ten sam Random:
public static double random() {
return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
}
private static final class RandomNumberGeneratorHolder {
static final Random randomNumberGenerator = new Random();
}
JavaDoc zaleca używanie klasy
SecureRandom dla „
kryptograficznie bezpiecznego generatora liczb pseudolosowych ”.
Bezpieczna losowa Java
Klasa
SecureRandom jest podklasą
java.util.Random i znajduje się w pakiecie
java.security . Porównanie tych dwóch klas można przeczytać w artykule „
Różnica między java.util.Random a java.security.SecureRandom ”. Dlaczego ten SecureRandom jest tak dobry? Faktem jest, że dla niego źródłem liczb losowych jest coś tak magicznie brzmiącego jak „pula entropii rdzenia”. Jest to zarówno plus, jak i minus. O wadach tego możesz przeczytać w artykule: „
Niebezpieczeństwa związane z java.security.SecureRandom ”. Krótko mówiąc, Linux ma generator liczb losowych w jądrze (RNG). RNG generuje liczby losowe na podstawie danych z puli entropii, która jest wypełniana na podstawie losowych zdarzeń w systemie, takich jak taktowanie klawiatury i dysku, ruchy myszy, przerwania i ruch sieciowy. Więcej informacji na temat puli entropii opisano w materiale „
Liczby losowe w systemie Linux (RNG) czyli jak „wypełnić” /dev/random i /dev/urandom . W systemach Windows używany jest SHA1PRNG zaimplementowany w sun.security.provider.SecureRandom. Wraz z rozwojem Javy zmienił się także SecureRandom, o czym warto przeczytać w recenzji „
Aktualizacje Java SecureRandom od kwietnia 2016 r. ”, aby uzyskać pełny obraz.
Wielowątkowość lub bądź jak Cezar
Jeśli spojrzysz na kod klasy
Random , nic nie wskazuje na kłopoty. Metody nie są oznaczone jako
zsynchronizowane . Jest jednak jedno ALE: tworząc
Random z domyślnym konstruktorem w kilku wątkach, udostępnimy między nimi to samo
ziarno instancji , dzięki któremu zostanie utworzony
Random . A także po otrzymaniu nowej liczby losowej zmienia się również wewnętrzny
AtomicLong instancji . Z jednej strony nie ma w tym nic złego z logicznego punktu widzenia, bo... Używany jest
AtomicLong . Z drugiej strony za wszystko, także za produktywność, trzeba płacić. I za to też. Dlatego nawet oficjalna dokumentacja
java.util.Random mówi: "
Instancje java.util.Random są bezpieczne dla wątków. Jednakże jednoczesne użycie tej samej instancji java.util.Random w różnych wątkach może spowodować konflikty, a w konsekwencji słabą wydajność. Weź pod uwagę zamiast tego używaj ThreadLocalRandom w projektach wielowątkowych ”. Oznacza to, że w aplikacjach wielowątkowych, gdy aktywnie korzystamy
z Random z kilku wątków, lepiej jest użyć klasy
ThreadLocalRandom . Jego użycie różni się nieco od zwykłego
losowego :
public static void main(String []args){
int rand = ThreadLocalRandom.current().nextInt(1,7);
System.out.println("Value: " + rand);
}
Jak widać, nie określamy dla niego
materiału siewnego . Ten przykład opisano w oficjalnym samouczku firmy Oracle:
Concurrent Random Numbers . Więcej o tej klasie przeczytasz w recenzji: „
Przewodnik po ThreadLocalRandom w Javie ”.
StreamAPI i losowe
Wraz z wydaniem Java 8 mamy wiele nowych funkcji. W tym Stream API. Zmiany wpłynęły również na generowanie wartości
losowych . Na przykład klasa
Random posiada nowe metody, które pozwalają uzyskać
Stream z losowymi wartościami, takimi jak
int
,
double
lub
long
. Na przykład:
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));
}
}
Dostępna jest także nowa klasa
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));
}
}
Więcej o różnicach pomiędzy
SplittableRandom i innymi klasami możesz przeczytać tutaj: „
Różne sposoby tworzenia liczb losowych w Javie ”.
Wniosek
Myślę, że warto wyciągnąć wnioski. Musisz uważnie przeczytać JavaDoc dla używanych klas. Za czymś tak prostym na pierwszy rzut oka jak Random kryją się niuanse, które mogą stanowić okrutny żart. #Wiaczesław
GO TO FULL VERSION