java:随机数
计算机科学中的随机数分为两种:
- 真随机数: 其生成过程是不可预测、不可重复的,完全基于自然界中物理过程的随机性。
- 伪随机数: 其生成过程是确定性的、可预测的,但通过一个算法使得生成的数字序列看起来像是随机的。
- 使用一个确定的数学公式。它从一个初始值(称为“种子”)开始,通过算法计算出下一个数,然后用这个数作为新的内部状态,继续计算下一个数,如此反复。
Java 随机数
Java 中的随机数有3种使用方式:
java.util.Random: 伪随机数java.util.concurrent.ThreadLocalRandom: 并发优化的Randomjava.security.SecureRandom: 真随机数
还有一种常用的方法获取随机数(Math.random()),查看源码可以发现 Math 实际上使用的还是 Random。
1 | public static double random() { |
线性同余伪随机算法
其中:
- ) 是序列中的第 n 个值,称为当前状态。
- 是序列中的第 n+1 个值,即下一个随机数。
- a 是乘数,一个非零整数。
- c 是增量。
- m 是模数,决定了随机数的范围(0 到 m-1)。
参数a, c, m以及初始值 (称为种子)共同决定了整个随机数序列。
线性同余生成器的质量几乎完全取决于参数 a, c, m的选择。糟糕的参数会导致非常差的随机性,例如:
- 周期极短:序列很快就开始重复。
- 相关性高:生成的数字之间有明显的模式。
为了获得尽可能长的周期(最长可达m)和良好的统计特性,参数通常需要满足以下条件(当 时):
- c 和 m 互质(最大公约数为 1)。
- a - 1 可以被 m 的所有质因数整除。
- 如果 m 是 4 的倍数,那么 a - 1 也必须是 4 的倍数。
历史上一个著名的、曾被广泛使用的例子是 C语言的 rand() 实现(在某些系统中):
- a = 1103515245
- c = 12345
- m =
Random
Random正是使用线性同余算法。
1 | public class Random implements java.io.Serializable { |
我们可以通过下面的代码 check(种子相同,结果一定相同):
1 | long seed = 123; |
SecureRandom
无论使用何种随机种子、进行何种精心计算和保护,都存在可能被推测出的风险。Java提供了 SecureRandom 随机数生成类,可以安全的生成真随机数。
- SecureRandom最多可生成128位随机值,提供了更高的安全性。
- SecureRandom 不使用固定种子值。而是从操作系统/dev/random 随机数文件中不断获取新的种子值。
SecureRandom并不是线程安全的,可以使用synchronized关键字同步,或者 使用ThreadLocal 为每个线程保存一个 SecureRandom实例。
但是,通常场景SecureRandom算法的耗时是Random的10倍以上。所以在实际使用中,应该仅在安全性较高的业务场景生成随机数时,使用 SecureRandom。
SecureRandom 几种策略
SHA1PRNG: SHA1PRNG是一种伪随机数生成器算法,在Java SecureRandom中,它被作为Windows下默认的随机数生成算法。该算法基于SHA-1算法,但通过添加额外的步骤来提高随机性。
NativePRNGBlocking: Java SecureRandom中,NativePRNG 算法是Linux 下默认的随机数生成算法。NativePRNGBlocking 初始播种时使用
/dev/random中的 20 个字节初始化内部 SHA1PRNG 实例,当调用 nextBytes()、nextInt() 等:使用内部 SHA1PRNG 实例的输出和从/dev/random读取的数据的进行异或。
NativePRNGBlocking每次计算随机数需要从/dev/random文件中获取数值。当/dev/random的随机数不足时,NativePRNGBlocking将会被阻塞。在桌面应用程序中,/dev/random文件很少会受到阻塞,因为它可以收集用户的鼠标、点击等事件。然而,在Web程序中,由于并发度较高,生成/dev/random数据可能会出现不足的情况。NativePRNGNonBlocking: 为了避免获取随机数被阻塞,NativePRNGNonBlocking选择从
/dev/urandom中获取随机数。/dev/urandom和/dev/random之间有一些区别。
/dev/random | /dev/urandom | |
|---|---|---|
| 随机性来源 | 系统熵池(环境噪音) | 初始基于熵池,不足时使用密码学安全的PRNG扩展 |
| 阻塞行为 | 是,当熵不足时会阻塞 | 否,永不阻塞 |
| 随机数质量 | 理论上随机性更高,适合极少数长期密钥生成 | 密码学安全,适合绝大多数场景 |
| 性能速度 | 慢 | 快 |
| 主要应用场景 | 生成极高强度、长寿命的密钥(如GPG/SSL/SSH根密钥) | 会话密钥、应用程序随机数、模拟测试、随机密码等绝大多数需求 |
在Linux中,无论是 /dev/random 还是 /dev/urandom,它们都共同使用一个被称为熵池 的随机源。你可以把它想象成一个由系统各种不可预测事件积累起来的"随机性仓库"。这些事件包括键盘敲击的时间间隔、鼠标移动的轨迹、硬盘读写的精确时刻,甚至包括网络数据包到达的纳秒级时间差。
ThreadLocalRandom
在 Random 中的源码中可以看到,种子值是存储在原子Long 中。在高并发场景,有可能会出现CAS操作的大量竞争失败自旋重试。在多线程中更建议使用ThreadLocalRandom。
ThreadLocalRandom 在 Thread 中增加了 3 个参数。
1 | // java.lang.Thread |
在 ThreadLocalRandom 使用时,会检查 Thread中属性有没有初始化,如果没有,会使用localInit()初始化 Thread 中的属性。
1 | // init seed |
用于在每个线程中存储种子。每次调用后,都会给种子增加一个固定值。
1 | final long nextSeed() { |