Random源码解读
问题
Random random = new Random(2);
System.out.println(random.nextInt(10));
这段代码指向一百次,请问会输出什么?
有的人可能会说,这我怎么知道?这是随机数呀!
但是我告诉你,答案只有一个!!
就是 8
原因
传入一个固定的 seed,经过nextInt()计算 也是唯一的。
有的人可能会疑惑了,这是为什么勒?它不是随机的么,那我们就去看一看源码
Random类
首先,我们先来看一看 nextInt()方法, 为了方便大家理解 我会将关键代码加上注释
nextInt(int bound)
public int nextInt(int bound) {
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
int r = next(31);
int m = bound - 1;
// bound 是 2 的n次方
if ((bound & m) == 0)
// bound = 2^n 先左移 n位 再右移动31位
r = (int)((bound * (long)r) >> 31);
else {
for (int u = r;
u - (r = u % bound) + m < 0;
u = next(31))
;
}
return r;
}
next 生成的值
- 如果 bound是 2 的幂次,先左移幂次(这种情况下,* 2的幂次方 = 左移幂次),再右移 31 位。返回值幂次位,即最大值为 0 ~ (2 的幂次 - 1) 位 == 0 ~ (bound - 1)
所以生成的值 范围为 [0 ~ (bound ) 左闭右开
- 如果不是,进入for循环;注意看这句 r = u % bound, 使得 r 值在[0 ~ (bound ) 左闭右开,不断地遍历 和 生成 u 和 r ,直至 next() 生成的值 + m 小于 r;
代码中频繁出现了 next(),现在我们去看看
next()
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));
// 类似于 48 - (48 - 31)== 31, 所以返回值是 31 位的数字
return (int)(nextseed >>> (48 - bits));
}
- 通过 oldseed 计算出 nextseed , 根据 mask 可以推断出 nextseed 的取值范围在 0 ~ (2 48 - 1)
private static final long mask = (1L << 48) - 1;
(1L << 48) - 1 是 48 位二进制(无符号位)可以表达的最大数字
-
使用 CAS 算法提交 nextseed ;若提交失败,自旋,从新计算 nextseed 再次提交
-
返回值范围 0 ~ 232 - 1,即int类型可取的正数。
传入 31 位 是因为,int 有一位符号位,所以有效表达正整数的位数是 31 位
谜底揭晓
以上的算法都没有展示出 Random 的 随机性,那么Random的随机性是如何保证和生成的? 接下来我们来看一看构造方法
分别是 有参数构造方法 和 无参构造方法
Random()
public Random() {
this(seedUniquifier() ^ System.nanoTime());
}
可以看到 无参构造方法调用的是 有参构造方法
同时, 我们还看见了一个 seedUniquifier() 函数,那么
Random 的随机性,是不是seedUniquifier() 实现的,我们来看一看
- seedUniquifier()
private static long seedUniquifier() {
// L'Ecuyer, "Tables of Linear Congruential Generators of
// Different Sizes and Good Lattice Structure", 1999
for (;;) {
long current = seedUniquifier.get();
long next = current * 1181783497276652981L;
if (seedUniquifier.compareAndSet(current, next))
return next;
}
}
可以看到 该函数,实现将 AtomicLong 关键字 seedUniquifier * 1181783497276652981L ,再重新提交
我们看一看 该关键字的初始值是从何而来, 可以看到
private static final AtomicLong seedUniquifier
= new AtomicLong(8682522807148012L);
总结:可以看到,该函数也没有实现随机性
那么随机性,在哪儿勒? 答案就是 System.nanoTime
System.nanoTime()
返回 任意时间的纳秒级数字(1 毫秒 = 1000 * 1000 纳秒)
表示自某个固定但任意的原始时间以来的纳秒(可能在未来,因此值可能为负)
与 System.currentTimeMillis() 的区别
nanoTime()表示从 1970.1.1 到现在的毫秒级
那么接下来我们再来看看 Random的有参构造方法
Random(int seed)
public Random(long seed) {
if (getClass() == Random.class)
this.seed = new AtomicLong(initialScramble(seed));
else {
// 子类需要初始化seed
this.seed = new AtomicLong();
setSeed(seed);
}
}
这里主要是对seed 全局的一个初始化。
总结
Random 实现随机性的核心 即是 seed,而seed的构造有两种方式,一种是 通过System.nanoTime()实现随机,一种是通过传入自己构造的seed,且这种方式是伪随机的。
但是自己传入的固定的seed,这种方法是 及其不安全的,
因为当你传入固定的seed,一旦seed泄露,你的随机数是可能被推导的!!,因为你的算法是不变的。当然使用系统构造的方法也可能有这种风险。
因此这种方式及其不安全的。
那么我们能通过什么方式实现安全的随机数么?
可以,答案是 使用SecureRandom ,它也是Random的子类,通过加入更多的“随机”的条件,如键盘输入、鼠标点击等事件推算出一个随机的seed。同时它还提供了多种安全算法。接下来我们来看一看它的实现吧
SecureRandom
SecureRandom secureRandom =
SecureRandom.getInstance("SHA1PRNG");
System.out.println(secureRandom.nextInt(19));
结尾
刚才敲代码时,将Random的API记错了,导致随机一直失败。因此打算写一篇博客警醒大家,哈哈。
如果你觉得这篇文章还不错,那就点个赞吧,当然也欢迎指出文中出现的问题。