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记错了,导致随机一直失败。因此打算写一篇博客警醒大家,哈哈。

如果你觉得这篇文章还不错,那就点个赞吧,当然也欢迎指出文中出现的问题。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值