CoderLi
2023/02/05阅读:35主题:极客黑
Java-随机数
分类
-
真随机数。通过物理实验得出。比如掷钱币、骰子、转轮、使用电子元件的噪音、核裂变等
-
伪随机数。通过一定算法和种子得出。软件实现的是伪随机数
-
强伪随机数。难以预测的随机数 -
弱伪随机数。易于预测的随机数
-
随机数的特性
-
随机性。不存在统计学偏差,完全是杂乱的数列 -
不可预测性。不能从过去的数列推测出下一个要出现的数 -
不可重现性。
弱伪随机数只需要满足随机性即可。
强伪随机数需要满足随机性和不可预测性。
真随机数需要同时满足三个特性。
应用场景
-
验证码生成 -
SessionId/Token 生成 -
密码
伪随机数的生成
伪随机数的生成实现一般是算法+种子
Pseudo Random Number Generator
伪随机数生成器(PRNG)
-
线性同余法 -
单向散列函数法 -
密码法
常见的是线性同余法,Java 中的 Random 类。
种子的选取
算法可以有很多种,伪随机数的强弱主要取决于种子。
比如 Random 的种子是系统当前的毫秒,所以它的随机数是可以预测的。
比如 SecureRandom 的种子在 Linux 下是选取 /dev/random 的、而它是手机计算机的各种信息得到的。比如键盘的输入、内存的使用状态、各种中断。
Linux操作系统的/dev/random设备接口
Windows操作系统的CryptGenRandom接口
Java 中的随机数

Random
如果不指定种子、则选取当前时间作为种子。
public Random(long seed) {
if (getClass() == Random.class)
this.seed = new AtomicLong(initialScramble(seed));
else {
// subclass might have overriden setSeed
this.seed = new AtomicLong();
setSeed(seed);
}
}
public int nextInt(int bound) {
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
// 根据旧种子生成新的种子
int r = next(31);
int m = bound - 1;
// 根据种子生成随机数
if ((bound & m) == 0) // i.e., bound is a power of 2
r = (int)((bound * (long)r) >> 31);
else {
for (int u = r;
u - (r = u % bound) + m < 0;
u = next(31))
;
}
return r;
}
通过 CAS 生成一个新的种子
protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
oldseed = seed.get();
nextseed = (oldseed * multiplier + addend) & mask;
} while (!seed.compareAndSet(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}
ThreadLocalRandom
public int nextInt(int bound) {
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
// 根据旧种子生成新的种子
int r = mix32(nextSeed());
int m = bound - 1;
// 根据新种子计算出随机数
if ((bound & m) == 0) // power of two
r &= m;
else { // reject over-represented candidates
for (int u = r >>> 1;
u + m - (r = u % bound) < 0;
u = mix32(nextSeed()) >>> 1)
;
}
return r;
}
依赖 ThreadLocal
final long nextSeed() {
Thread t; long r; // read and update per-thread seed
UNSAFE.putLong(t = Thread.currentThread(), SEED,
r = UNSAFE.getLong(t, SEED) + GAMMA);
return r;
}
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> tk = Thread.class;
SEED = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSeed"));
PROBE = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomProbe"));
SECONDARY = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSecondarySeed"));
} catch (Exception e) {
throw new Error(e);
}
}
我们去 Thread 类中

Random 和 ThreadLocalRandom 比较

SecureRandom
SecureRandom 默认支持两种加密算法:
SHA1PRNG 算法,提供者 sun.security.provider.SecureRandom。Window 下
NativePRNG 算法,提供者 sun.security.provider.NativePRNG。Linux 下
/dev/random和/dev/urandom
/dev/random和/dev/urandom是unix系统提供的产生随机数的设备,很多应用都需要使用random设备提供的随机数,比如ssh keys, SSL keys, TCP/IP sequence numbers 等等。
# 输出所有的值
cat /dev/random | od -x
# 输出总熵
cat /proc/sys/kernel/random/entropy_avail

阻塞问题
soloar代码检测
// 原代码
public void doSomethingCommon() {
Random rand = new Random(); // Noncompliant; new instance created with each invocation
int rValue = rand.nextInt();
//...
// 提出的建议
private Random rand = SecureRandom.getInstanceStrong(); // SecureRandom is preferred to Random
public void doSomethingCommon() {
int rValue = this.rand.nextInt();
//...
代码复现
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
public class TestRandom {
public static void main(String[] args) throws NoSuchAlgorithmException {
System.out.println("start.....");
long start = System.currentTimeMillis();
SecureRandom random = SecureRandom.getInstanceStrong();
for(int i = 0; i < 100; i++) {
System.out.println("第" + i + "个随机数.");
random.nextInt(10000);
}
System.out.println("finish...time/ms:" + (System.currentTimeMillis() - start));
}
}
Windows 和 Mac 无法复现。Linux 会出现阻塞
jstack 看线程和栈
"main" #1 prio=5 os_prio=0 tid=0x00007f894c009000 nid=0x1129 runnable [0x00007f8952aa9000]
java.lang.Thread.State: RUNNABLE
at java.io.FileInputStream.readBytes(Native Method)
at java.io.FileInputStream.read(FileInputStream.java:255)
at sun.security.provider.NativePRNG$RandomIO.readFully(NativePRNG.java:424)
at sun.security.provider.NativePRNG$RandomIO.ensureBufferValid(NativePRNG.java:525)
at sun.security.provider.NativePRNG$RandomIO.implNextBytes(NativePRNG.java:544)
- locked <0x000000076c77cb28> (a java.lang.Object)
at sun.security.provider.NativePRNG$RandomIO.access$400(NativePRNG.java:331)
at sun.security.provider.NativePRNG$Blocking.engineNextBytes(NativePRNG.java:268)
at java.security.SecureRandom.nextBytes(SecureRandom.java:468)
at java.security.SecureRandom.next(SecureRandom.java:491)
at java.util.Random.nextInt(Random.java:390)
at TestRandom.main(TestRandom.java:12)
这里注意到是它会持有一个 Java 锁、其他线程调用该方法时会获取不到锁、然后阻塞
// name of the pure random file (also used for setSeed())
private static final String NAME_RANDOM = "/dev/random";
// name of the pseudo random file
private static final String NAME_URANDOM = "/dev/urandom";
private static RandomIO initIO(final Variant v) {
return AccessController.doPrivileged(
new PrivilegedAction<RandomIO>() {
@Override
public RandomIO run() {
File seedFile;
File nextFile;
switch(v) {
//...忽略中间代码
case BLOCKING: // blocking状态下从/dev/random文件中读取
seedFile = new File(NAME_RANDOM);
nextFile = new File(NAME_RANDOM);
break;
case NONBLOCKING: // unblocking状态下从/dev/urandom文件中读取数据
seedFile = new File(NAME_URANDOM);
nextFile = new File(NAME_URANDOM);
break;
//...忽略中间代码
try {
return new RandomIO(seedFile, nextFile);
} catch (Exception e) {
return null;
}
}
});
}
// constructor, called only once from initIO()
private RandomIO(File seedFile, File nextFile) throws IOException {
this.seedFile = seedFile;
seedIn = new FileInputStream(seedFile);
nextIn = new FileInputStream(nextFile);
nextBuffer = new byte[BUFFER_SIZE];
}
private void ensureBufferValid() throws IOException {
long time = System.currentTimeMillis();
if ((buffered > 0) && (time - lastRead < MAX_BUFFER_TIME)) {
return;
}
lastRead = time;
readFully(nextIn, nextBuffer);
buffered = nextBuffer.length;
}
直接尝试读取 /dev/random 文件。
同样会产生阻塞
import java.io.FileInputStream;
import java.io.IOException;
public class TestReadUrandom {
public static void main(String[] args) throws IOException {
System.out.println("start.....");
for(int i = 0; i < 100; i++) {
System.out.println("第" + i + "次读取随机数");
FileInputStream inputStream = new FileInputStream("/dev/random");
byte[] buf = new byte[32];
inputStream.read(buf, 0, buf.length);
}
}
}
c 语言读取。同样阻塞
int main() {
int randnum = 0;
int fd = open("/dev/random", O_RDONLY);
if(fd == -1) {
printf("open error.\n");
return 1;
}
int i = 0;
for(i = 0; i < 100; i++) {
read(fd, (char *)&randnum, sizeof(int));
printf("random number = %d\n", randnum);
}
close(fd);
return 0;
}
-
不推荐使用SecureRandom.getInstanceStrong()方式获取SecureRandom(除非对随机要求很高)
-
推荐使用new SecureRandom()获取SecureRandom, linux下从/dev/urandom读取. 虽然是伪随机, 但大部分场景下都满足。或者使用 SecureRandom.getInstance("NativePRNGNonBlocking")
虚拟机环境下和服务器情况类似,输入设备操作很少,相对于 Host 而言,Disk I/O 也相对较少,因此依赖 Guest 自身 PRNG 产生的随机数质量不高,因此虚拟机通常从 Host(宿主机)获取部分随机数据。对于 KVM 虚拟机来说,存在一个半虚拟化设备 virtio-rng 作为硬件随机数产生器。Linux Kernel 从 2.6.26 开始支持 virtio-rng, QEMU 在 1.3 版本加入了对 virtio-rng 的支持。 virtio-rng 设备会读取 Host 的随机数源并且填充到 Guest(客户机)的熵池中。
https://juejin.cn/post/6844903572883128327
https://juejin.cn/s/安全随机数的要求是不可预测%2C绝对安全
https://juejin.cn/post/6844904096785235982
https://blog.csdn.net/fengye_csdn/article/details/120843570
https://blog.csdn.net/weixin_45244678/article/details/106137948
作者介绍