随机数的生成
通常随机数会通过 Random
类、ThreadLocalRandom
类、SecureRandom
类或者 Math.random()
方法生成
Random
1 | import java.util.Random; |
ThreadLocalRandom
1 | import java.util.concurrent.ThreadLocalRandom; |
SecureRandom
1 | import java.security.SecureRandom; |
Math.random
1 | public class MathRandomExample { |
优缺点
四种生成方式在性能和安全性方面的优缺点比较
Random
Random 使用了一种简单且高效的线性同余生成器(LCG)算法来生成 ==伪随机数==
$$
seed_{n+1} = (seed_n \times a + c) mod m
$$
这样可以带来较高的性能,但缺点是会导致随机数可被预测
如果两个 Random 实例使用相同的种子进行初始化,那么它们生成的随机数序列将完全相同
1 | import java.util.Random; |
输出结果将显示两个实例生成的随机数序列完全相同:
1 | rand1: |
Random 是 ==线程安全== 的
1 | protected int next(int bits) { |
Random 通过 CAS 确保线程安全
ThreadLocalRandom
ThreadLocalRandom 与 Random 的特性类似,主要的区别在于 ThreadLocalRandom 解决了 Random 在高并发场景下性能不足的问题
Radom 通过 CAS 来解决线程安全问题,但 CAS 在线程竞争比较激烈的场景中性能低下,原因是 CAS 对比时如果经常有其他线程修改原来的值,就会导致 CAS 对比失败,于是一直自旋进行 CAS
而 ThreadLocalRandom 利用 ThreadLocal 为每个线程分配一个独立的随机数生成实例,避免了线程间的竞争。另外,为了进一步优化性能,ThreadLocalRandom 利用缓存行填充技术来防止伪共享(false sharing)
CPU 与内存间存在 L1,L2,L3 三级缓存,CPU 在执行运算的时候,首先去 L1 查找所需数据,其次是 L2,L3,如果缓存中都没有,就去内存中拿。走得越远,耗时越长
由于共享变量在 CPU 缓存中的存储是以
缓存行
为单位,一个缓存行可以存储多个变量(存满当前缓存行的字节数)而 CPU 对缓存的修改又是以
缓存行
为最小单位的,在多线程情况下,如果需要修改「共享同一个缓存行的变量」,就会无意中影响彼此的性能,这就是伪共享(False Sharing)
ThreadLocalRandom 虽然提高了 Random 的性能,但仍是统计学上的随机数,是可预测的随机数
SecureRandom
要想获得密码学上的随机性,需要满足更严格的要求:
- 不可预测性:即使攻击者知道了一部分随机数,也无法推算出其他部分
- 熵源的复杂性:需要复杂和高质量的熵源,确保生成的随机数无法被逆向工程
- 安全算法:使用复杂的算法,增加攻击和预测的难度
SecureRandom 不同于 Random 使用系统当前时间作为种子,SecureRandom 使用 随机事件 作为种子,比如:鼠标点击,键盘点击等
这一点十分重要,也是后面导致 SecureRandom 引发线程阻塞的原因
SecureRandom 引发的线程阻塞
起因是,通过如下代码实现了一个验证码生成的功能
1 | private Object[] genVerificationCodeGraph() { |
代码运行并无任何问题,不过 SonarLint 却提示代码存在缺陷,SonarLint 建议如下
Noncompliant Code Example
1 | public void doSomethingCommon() { |
Compliant Solution
1 | private Random rand = SecureRandom.getInstanceStrong(); // SecureRandom is preferred to Random |
当时并没过多思考,便直接将 SonarLint 建议的直接应用了
发布到测试环境进行测试时就出现了十分怪异的现象,获取验证码的接口一直无响应,重启应用也无济于事,但我本地运行又是没有任何问题的
到测试环境通过 jstack 查看线程状态,发现线程都阻塞在 SecureRandom 中
原因
原因就出在操作系统上,SecureRandom generateSeed()
使用 /dev/random
生成种子,/dev/random
是一个从环境噪声中收集熵(如键盘输入、鼠标移动、磁盘操作等)来生成随机数的 阻塞 数字生成器,如果它没有足够的随机数据提供,它就一直等待
类似的,还存在一个
/dev/urandom
,一个非阻塞随机数生成器,它适用于需要随机数但不要求极高安全性的场合
解决
最直接的方式
不使用 SecureRandom,换为 Random 或 ThreadLocalRandom
修改配置
1
2
3-Djava.security.egd=file:/dev/urandom
或者
-Djava.security.egd=file:/dev/./urandom代码中显式指定非阻塞算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
public class SecureRandomExample {
public static void main(String[] args) {
try {
// 使用指定的算法 "NativePRNGNonBlocking" 以避免阻塞
SecureRandom secureRandom = SecureRandom.getInstance("NativePRNGNonBlocking");
System.out.println("Random Integer: " + secureRandom.nextInt());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
}使用第三方库增加熵源
可以使用
haveged
或rng-tools
来增加系统的熵源,从而减少阻塞的概率安装
haveged
1
2
3sudo apt-get install haveged
sudo systemctl enable haveged
sudo systemctl start haveged安装
rng-tools
1
2
3sudo apt-get install rng-tools
sudo systemctl enable rngd
sudo systemctl start rngd