java.util.Random 实现原理

作者: jijs | 来源:发表于2017-08-30 00:01 被阅读686次

    概述

    该类的实例被用于生成伪随机数的流。该类使用一个 48 位的种子,它被一个线性同余公式所修改。如果 Random 的两个实例用同一种子创建,对每个实例完成同方法调用序列它们将生成和返回相同的数序列成同一方法调用序列,它们将生成和返回相同的数序列。

    示例

    public class RandomTest {
        public static void main(String[] args) {
            testRandom();
            System.out.println("---------------------");
            testRandom();
            System.out.println("---------------------");
            testRandom();
        }
        
        public static void testRandom(){
            Random random = new Random(1);
            for(int i=0; i<5; i++){
                System.out.print(random.nextInt()+"\t");
            }
            System.out.println("");
        }
    }
    

    输出结果:

    从结果中发现,只要种子一样,获取的随机数的序列就是一致的。是一种伪随机数的实现,而不是真正的随机数。

    Random 源码分析

    Random 类结构

    class Random implements java.io.Serializable {
        private final AtomicLong seed;
    
        private static final long multiplier = 0x5DEECE66DL;
        private static final long addend = 0xBL;
        private static final long mask = (1L << 48) - 1;
        private static final AtomicLong seedUniquifier = new AtomicLong(8682522807148012L);
    

    有参构造方法

    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);
        }
    }
    
    private static long initialScramble(long seed) {
        return (seed ^ multiplier) & mask;
    }
    

    通过传入一个种子,来生成随机数,通过上面的例子发现,种子一样产生的随机数序列一样,如果每次使用想产生不一样的序列,那就只能每次传入一个不一样的种子。

    无参构造方法

    public Random() {
        this(seedUniquifier() ^ System.nanoTime());
     }
    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 * 181783497276652981L;
            if (seedUniquifier.compareAndSet(current, next))
                return next;
        }
    }
    

    通过源码发现,无参的构造方法,里面帮我们自动产生了一个种子,并通过CAS自旋方式保证,每次获取的种子不一样,从而保证每次new Random()获取的随机序列不一致。

    nextInt() 方法:获取 int 随机数

    public int nextInt() {
        return next(32);
    }
    
    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));
    }
    

    从代码中我们可以发现,只要种子确定后,每次产生的数,都是采用固定的算法进行产生的,所以只要种子确定后,每次产生的序列就是固定的。

    每次更新种子的时候是使用的CAS来更新的,如果高并发的环境下,性能是个问题。

    安全性问题

    试想下,如果这是一个摇奖平台,只要种子确定后,每次产生的序列都一样。这样就可利用这个漏洞来预测下一次开奖的号码,这样容易被一些人钻空子。

    jdk建议大家尽量要使用 SecureRandom 来实现随机数的生成。

    SecureRandom

    SecureRandom是强随机数生成器,主要应用的场景为:用于安全目的的数据数,例如生成秘钥或者会话标示(session ID),在上文《伪随机数安全性》中,已经给大家揭露了弱随机数生成器的安全问题,而使用SecureRandom这样的强随机数生成器将会极大的降低出问题的风险。

    产生高强度的随机数,有两个重要的因素:种子和算法。算法是可以有很多的,通常如何选择种子是非常关键的因素。 如Random,它的种子是System.currentTimeMillis(),所以它的随机数都是可预测的, 是弱伪随机数。
    强伪随机数的生成思路:收集计算机的各种信息,键盘输入时间,内存使用状态,硬盘空闲空间,IO延时,进程数量,线程数量等信息,CPU时钟,来得到一个近似随机的种子,主要是达到不可预测性。

    说的简单点就是,使用加密算法生成很长的一个随机种子,让你无法猜测出种子,也就无法推导出随机序列数。

    Random性能问题

    从 Random 源码中我们发现,每次获取随机数的时候都是使用CAS的方式进行更新种子的值。这样在高并发的环境中会存在大量的CAS重试,导致性能下降。这时建议大家使用ThreadLocalRandom类来实现随机数的生成。

    ThreadLocalRandom 实现原理

    Thread 类


    Thread 类中有一个 threadLocalRandomSeed 属性。

    ThreadLocalRandom 结构


    SEED 变量是 threadLocalRandomSeed 在 Thread 对象中的偏移量。

    ThreadLocalRandom.nextSeed() 方法


    从这个方法中,我们发现,每个线程的种子值都存储在Thread对象的threadLocalRandomSeed 属性中。

    结论

    因为ThreadLocalRandom 中的种子存储在Thread对象中,所以高并发获取Random对象时,不会使用CAS来保证每次获取的值不一致。
    每个线程维护一个它自己的种子,每个线程需要获取随机数的时候,从当前的Thread对象中获取当前线程的种子,进行获取随机数,性能大大提高。


    想了解更多精彩内容请关注我的公众号

    相关文章

      网友评论

      • 呵呵是种态度:个人还是感觉cas好点。threadlocal在您所说的高并发的场景下也要考虑内存消耗问题不是吗
        呵呵是种态度:@jijs 好的谢谢。我研究一下
        jijs:使用ThreadlocalRandom 是不会出现内存消耗问题的。
        因为ThreadlocalRandom 是多个线程公用的,只有一个对象存在。
        种子是存在Thread类中的,是long类型的,就算你不存种子,他也在内存中占用64位,存放种子也是在内存中占64位。
      • 金戈大王:好文章👍

      本文标题:java.util.Random 实现原理

      本文链接:https://www.haomeiwen.com/subject/iykodxtx.html