介绍
世界上有许多科学研究概率论。科学由不同的部分组成。例如,在数学中,有一个单独的部分专门研究随机事件、数量等。但科学并没有被轻视。在这种情况下,当人们试图了解玩机会游戏时掷骰子的模式时,概率论开始成形。如果你仔细观察,我们周围有很多看似随机的事情。但一切随机的事情并不完全随机。但稍后会详细介绍。从 JDK 的第一个版本开始,Java 编程语言也支持随机数。
Java 中的随机数可以通过java.util.Random类来使用。为了进行测试,我们将使用
tutorialspoint java在线编译器。
下面是一个使用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 API中
java.util.Random类的描述。在这里我们看到了有趣的事情。
Random类使用伪随机数。为何如此?原来随机数并没有那么随机?
伪随机性 java.util.Random
java.util.Random类的文档表示,如果使用相同的
种子参数创建
Random实例并对实例执行相同的操作序列,则它们将返回相同的数字序列。如果我们仔细观察,我们可以看到
Random实际上有
一个构造函数,它以一些
long值作为
种子:
Random rnd1 = new Random(1L);
Random rnd2 = new Random(1L);
boolean test = rnd1.nextInt(6) == rnd2.nextInt(6);
System.out.println("Test: " + test);
这个例子将返回
true因为
两个实例的种子是相同的。该怎么办?默认构造函数部分解决了这个问题。
以下是Random构造函数的内容示例:
public Random() {
this(seedUniquifier() ^ System.nanoTime());
}
默认构造函数使用按位异
或运算。并使用代表当前时间的
long和一些种子:
private static long seedUniquifier() {
for (;;) {
long current = seedUniquifier.get();
long next = current * 181783497276652981L;
if (seedUniquifier.compareAndSet(current, next))
return next;
}
}
这里另一个有趣的事情是,每次调用
SeedUniquifier getter 方法都会更改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);
}
但如果你仔细观察,会发现里面有同样的随机数:
public static double random() {
return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
}
private static final class RandomNumberGeneratorHolder {
static final Random randomNumberGenerator = new Random();
}
JavaDoc 建议使用
SecureRandom类作为“
加密安全伪随机数生成器”。
安全随机 Java
SecureRandom类是
java.util.Random的子类,位于
java.security包中。这两个类的比较可以阅读文章“
java.util.Random 和 java.security.SecureRandom 之间的差异”。为什么这个 SecureRandom 这么好?事实上,对他来说,随机数的来源就是“核心熵池”这样一个听起来很神奇的东西。这既是优点也是缺点。您可以在文章“
java.security.SecureRandom 的危险”中了解其缺点。简而言之,Linux 有一个内核随机数生成器(RNG)。RNG 根据熵池中的数据生成随机数,熵池根据系统中的随机事件填充,例如键盘和磁盘计时、鼠标移动、中断和网络流量。有关熵池的更多信息,请参阅材料“
Linux 中的随机数 (RNG) 或如何“填充”/dev/random 和 /dev/urandom ”。在 Windows 系统上,使用 SHA1PRNG,在 sun.security.provider.SecureRandom 中实现。随着 Java 的发展,SecureRandom 也发生了变化,值得阅读评论“
截至 2016 年 4 月的 Java SecureRandom 更新”以了解完整情况。
多线程或者像凯撒一样
如果您查看
Random类的代码,似乎没有任何迹象表明有问题。方法未标记
为同步。但有一个 BUT:当在多个线程中使用默认构造函数创建
Random时,我们将在它们之间共享相同的实例种子,通过该种子来创建
Random。而且当收到新的随机数时,
实例的内部AtomicLong也会发生变化。一方面,从逻辑的角度来看,这并没有什么问题,因为…… 使用
AtomicLong。另一方面,你必须为一切付出代价,包括生产力。也是为了这个。
因此,即使java.util.Random的官方文档也说:“
java.util.Random 的实例是线程安全的。但是,跨线程并发使用同一个 java.util.Random 实例可能会遇到争用,从而导致性能不佳。考虑相反,在多线程设计中使用 ThreadLocalRandom ”。也就是说,在多线程应用程序中,当从多个线程主动使用
Random时,最好使用
ThreadLocalRandom类。
它的用法与常规Random略有不同:
public static void main(String []args){
int rand = ThreadLocalRandom.current().nextInt(1,7);
System.out.println("Value: " + rand);
}
正如您所看到的,我们没有为其指定
种子。Oracle 的官方教程:
Concurrent Random Numbers中描述了此示例。您可以在评论中阅读有关此类的更多信息:“
Guide to ThreadLocalRandom in Java ”。
StreamAPI 和随机
随着 Java 8 的发布,我们有了许多新功能。包括流API。
这些变化也影响了随机值的生成。
例如, 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和其他类 之间的区别的更多信息:“
在 Java 中创建随机数的不同方法”。
结论
我认为值得得出结论。您需要仔细阅读所使用的类的 JavaDoc。像“随机”这样乍一看很简单的东西背后,隐藏着可能会开一个残酷玩笑的细微差别。#维亚切斯拉夫
GO TO FULL VERSION