Java中生成随机数的4种方式及其应用场景详解
在 Java 里,生成随机数的情况有诸多。今日,小华将给大家分享 4 种生成随机数的办法,还有它们彼此之间的差异以及每种生成方式所对应的场合。

1、Random
Random 类在 JDK 1.0 时诞生。它所产生的随机数是伪随机数,也就是具有一定规则的随机数。Random 使用的随机算法是 linear congruential pseudorandom number generator (LGC) 线性同余法伪随机数。在进行随机数生成时,起始的那个用于生成随机数的数字叫做种子数。以种子数为基础,进行特定的变换,这样就能产生所需要的随机数字。
Random 对象在种子数相同的情形下,相同次数所生成的随机数是一样的。例如有两个种子数相同的 Random 对象,它们第一次生成的随机数字是完全一样的,第二次生成的随机数字也同样是完全一样的。在默认状态下,new Random()是以当前的纳秒时间当作种子数的。
① 基础使用
生成一个从 0 到 10 的随机数(不包含 10),通过 Random 来实现,代码如下:
② 优缺点分析
Random 使用 LGC 算法生成伪随机数,这种方式的优点在于执行效率较高,并且生成的速度较快。
它的缺点在于,倘若 Random 的随机种子相同,那么每次生成的随机数都能够被预测到,也就是每次生成的随机数都是一样的。就像下面的代码所展示的那样,当我们给两个线程设定相同的种子数时,就会发现每次产生的随机数也是相同的。
③ 线程安全问题
当我们要使用一个类的时候,首先关心的一个问题是:它是否为线程安全的?就 Random 而言,Random 是线程安全的。
PS:在多线程的场景下,如果程序的执行结果和预期的结果一致,那么就称其为线程安全的;如果程序的执行结果和预期的结果不一致,那么就称为非线程安全的(也叫线程安全问题)。假设有两个线程,其中一个线程进行 10 万次的 ++ 操作,另一个线程进行 10 万次的 -- 操作。这样一来,最终的结果应该是既没有增加也没有减少。倘若程序最终的结果与预期不相符合,那就说明该程序不是线程安全的。
我们来看 Random 的实现源码:
PS:本文所有源码来自于 JDK 1.8.0_211。
从以上源码能够看出,Random 在底层是利用 CAS(比较并替换)来处理线程安全问题的。所以,对于绝大多数随机数生成的情形而言,使用 Random 不失为一种很不错的选择。
Java 并发机制实现原子操作有两种方式。一种方式是使用锁。另一种方式是使用 CAS。CAS 是“比较并替换”的缩写。在 java.util.concurrent.atomic 中,有很多类,比如 AtomicInteger、AtomicBoolean、AtomicLong 等,这些类都运用了 CAS 机制来达成目的。
2、ThreadLocalRandom
ThreadLocalRandom 是在 JDK 1.7 时新提供的一个类,它是 JUC(java.util.concurrent)中的一员。既然已经有了 Random,那为什么还要再创建一个 ThreadLocalRandom 呢?
原因很简单。从上面 Random 的源码能够看出,Random 在生成随机数时,利用 CAS 来处理线程安全问题。然而,在线程竞争较为激烈的场景里,CAS 的效率很低。这是因为在 CAS 进行对比时,经常有其他线程在修改原来的值,从而致使 CAS 对比失败。所以,它需要一直循环,以尝试进行 CAS 操作。在多线程竞争激烈的场景下,可以使用 ThreadLocalRandom 来解决 Random 执行效率低的问题。
我们第一眼看到 ThreadLocalRandom 时,一定会联想到一种类似 ThreadLocal 的东西。确实是这样的。ThreadLocalRandom 的实现原理和 ThreadLocal 相近,它能给每个线程都提供一个属于自己的本地种子,这样就能避免多个线程去竞争一个种子,进而避免了因这种竞争而产生的额外性能开销。
① 基础使用
接下来我们运用 ThreadLocalRandom 去生成一个处于 0 到 10 之间的随机数,这个随机数不包含 10。实现的代码如下:
② 实现原理
ThreadLocalRandom 的实现原理与 ThreadLocal 相近。它的做法是让每个线程都拥有自己的本地种子,并且只有在生成随机数的时候才会对这个种子进行初始化。其实现的源码如下:
③ 优缺点分析
ThreadLocalRandom 把 Random 和 ThreadLocal 类结合在一起,并且被隔离开了,是在当前线程当中。所以它通过避免对种子数进行竞争操作,这样就在多线程运行的环境里实现了更好的性能,同时也保证了自身的线程安全。
另外,Random 与之不同,ThreadLocalRandom 明确表明不支持设置随机种子。它对 Random 的 setSeed(long seed) 方法进行了重写,并且直接抛出了 UnsupportedOperationException 异常,正因如此,降低了多个线程出现随机数重复的可能性。
源码如下:
程序中调用 setSeed() 方法就会抛出 UnsupportedOperationException 异常,这一点可由下图所示。
ThreadLocalRandom 缺点分析
ThreadLocalRandom 不具备手动设置随机种子的方法,然而这并不意味着 ThreadLocalRandom 是毫无瑕疵的。当我们去查看 ThreadLocalRandom 初始化随机种子的方法 initialSeed() 的源码时,能够发现,在默认情形下,它的随机种子是与当前时间相关的,源码如下:
从上述源码能够得知,若我们设定了启动参数“-Djava.util.secureRandomSeed=true”,此时 ThreadLocalRandom 将会生成一个随机种子,这样在一定程度上可以减轻因随机种子相同而致使随机数可被预测的问题。不过,在默认的情形下,如果不设置这个参数,那么在多线程环境中,由于启动时间相同,就可能会导致多个线程在每一步操作中都生成相同的随机数。
3、SecureRandom
SecureRandom 是从 Random 继承而来的。这个类能够提供加密强度高的随机数生成器。SecureRandom 与 Random 有所不同,它会收集诸如鼠标点击、键盘点击等一些随机事件。并且,SecureRandom 会利用这些随机事件当作种子来使用。这意味着种子具有不可预测性。与 Random 默认使用系统当前时间的毫秒数作为种子不同,这样就避免了生成相同随机数的可能性。
基础使用
SecureRandom 默认支持两种加密算法:
SHA1PRNG 算法的提供者是 sun.security.provider.SecureRandom;NativePRNG 算法的提供者是 sun.security.provider.NativePRNG。
除了上述的操作方式以外,你还能够选择运用 new SecureRandom() 去创建 SecureRandom 对象,具体实现的代码如下:
使用 new 来初始化 SecureRandom 时,默认会运用 NativePRNG 算法去生成随机数。不过,能够通过配置 JVM 启动参数“-Djava.security”来对生成随机数的算法进行修改,也可以选择使用 getInstance("算法名称")这种方式来指定生成随机数的算法。
4、Math
Math 类在 JDK 1.0 时诞生。它包含用于执行基本数学运算的属性和方法,像初等指数、对数、平方根和三角函数等。同时,它也包含生成随机数的静态方法 Math.random(),此方法能产生一个 0 到 1 的 double 值,如下代码所示。
① 基础使用
以上程序的执行结果为:
② 扩展
当然,你若想用它来生成一定范围的 int 值,这也是可行的。你可以进行如下书写:
以上程序的执行结果为:
③ 实现原理
分析 Math 的源码后可知:第一次调用 Math.random() 方法时,会自动创建一个伪随机数生成器,此生成器实际上是 new java.util.Random()。当下一次再次调用 Math.random() 方法时,就会使用这个新创建的伪随机数生成器。
源码如下: