JAVA中Random分析

JAVA中提供了几个常用的用于生成随机数(流,JDK 8支持)的API:Random,ThreadLocalRandom,SecureRandom,SplittableRandom;这几个类在使用场景上,稍微有些区分。

Random

  1. 生成伪随机数(流),使用48位种子,使用线性同余公式进行修改。可以通过构造器传入初始seed,或者通过setSeed重置(同步);默认seed生成主导变量为系统时间的纳秒数。
    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;
        }
    }
  1. 如果两个(多个)不同的Random实例,使用相同的seed,按照相同的顺序调用相同方法,那么它们得到的数字序列也是相同的。这种设计策略,既有优点也有缺点,优点是“相同seed”生成的序列是一致的,使过程具有可回溯和校验性(平台无关、运行时机无关);缺点就是,这种一致性,潜在引入其“可被预测”的风险。

  2. Random的实例是线程安全的。 但是,跨线程并发使用相同的java.util.Random实例可能会遇到争用,从而导致性能稍欠佳(nextX方法中,在对seed赋值时使用了CAS,测试结果显示,其实性能损耗很小)。 请考虑在多线程设计中使用ThreadLocalRandom。同时,我们在并发环境下,也没有必要刻意使用多个Random实例。

  3. Random实例不具有加密安全性。 相反,请考虑使用SecureRandom来获取加密安全的伪随机数生成器,以供安全敏感应用程序使用。

Randoms是最常用的随机数生成类,适用于绝大部分场景。

Random random = new Random(100);  
System.out.println(random.nextInt(10) + "," + random.nextInt(30) + "," + random.nextInt(50));  
  
random = new Random(100);  
System.out.println(random.nextInt(10) + "," + random.nextInt(30) + "," + random.nextInt(50));  
  
random = new Random(100);  
System.out.println(random.nextInt(10) + "," + random.nextInt(30) + "," + random.nextInt(50));

上述三个不同的random实例,使用了相同的seed,调用过程一样,其中产生的随机数序列也是完全一样的。多次执行结果也完全一致,简单而言,只要初始seed一样,即使实例不同,多次运行它们的结果都是一致的

变更seed的代码:

    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));
    }

注意:seed的变更是原子的,不会产生线程安全问题,先更新seed,在通过这个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;
    }

没有变量参与,所以只要是seed一致,产生的随机数就一致。

如果Random构造器中不指定seed,而是使用默认的系统时间纳秒数作为主导变量,三个random实例执行的结果是不同的。多次执行结果也不一样。由此可见,seed是否具有随机性,在一定程度上,也决定了Random产生结果的随机性

所以,在分布式或者多线程环境下,如果Random实例处于代码一致的tasks线程中,可能这些分布式进程或者线程,产出的序列值可能是一样的(因为时间参与的随机数的产生,如果同时执行,seed应该是一样的)。所以,这也是在JDK 7引入ForkJoin的同时,也引入了ThreadLocalRandom类。

ThreadLocalRandom

随机数生成器隔离到当前线程,此类继承自java.util.Random。 与Math类使用的全局Random生成器一样,ThreadLocalRandom使用内部生成的种子进行初始化,否则可能无法修改。

    public static ThreadLocalRandom current() {
        if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
            localInit();
        return instance;
    }
    static final void localInit() {
        int p = probeGenerator.addAndGet(PROBE_INCREMENT);
        int probe = (p == 0) ? 1 : p; // skip 0
        long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
        Thread t = Thread.currentThread();
        UNSAFE.putLong(t, SEED, seed);
        UNSAFE.putInt(t, PROBE, probe);
    }

在并发程序中使用ThreadLocalRandom而不是共享Random对象,通常会更少的开销和争用。 当多个任务(例如,每个ForkJoinTask)在线程池中并行使用随机数时,使用ThreadLocalRandom是特别合适的。
此类的用法通常应为以下形式:ThreadLocalRandom.current().nextX()。 切记,在多个线程中不应该共享ThreadLocalRandom实例,即我们遵循上述方法调用方式来获取随机数。

ThreadLocalRandom初始化过程也被关闭,所以无法通过构造器设定seed,此外其setSeed方法也被重写而不支持(抛出异常),默认情况下,每个ThreadLocalRandom实例的seed主导变量值为系统时间(纳秒):

private static final AtomicLong seeder = new AtomicLong(initialSeed());

private static long initialSeed() {
String sec = VM.getSavedProperty(“java.util.secureRandomSeed”);
if (Boolean.parseBoolean(sec)) {
byte[] seedBytes = java.security.SecureRandom.getSeed(8);
long s = (long)(seedBytes[0]) & 0xffL;
for (int i = 1; i < 8; ++i)
s = (s << 8) | ((long)(seedBytes[i]) & 0xffL);
return s;
}
return (mix64(System.currentTimeMillis()) ^
mix64(System.nanoTime()));
}

根据其初始化seed的实现,我们也可以通过JVM启动参数增加“-Djava.util.secureRandomSeed=true”,此时初始seed变量将不再是系统时间,而是由SecureRandom类生成一个随机因子,以此作为ThreadLoalRandom的初始seed,相对更加安全。

从源码中,我并没有看到Thread-ID作为变量生成seed,而且nextX方法中随机数生成算法也具有一致性,这意味着,如果多个线程初始ThreadLocalRandom的时间完全一致,在调用方法和过程相同的情况下,产生的随机序列也是相同的;在一定程度上“-Djava.util.secureRandom=true”可以规避此问题。

ThreadLocalRandom并没有使用ThreadLocal来支持内部数据存储等,而是直接使用UnSafe操作当前Thread对象引用中seed属性的内存地址并进行数据操作,我比较佩服SUN的这种巧妙的做法。

 private static final sun.misc.Unsafe UNSAFE;
    private static final long SEED;
    private static final long PROBE;
    private static final long SECONDARY;
    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);
        }
    }

SecureRandom

继承自Random,该类提供加密强随机数生成器(RNG),加密强随机数最低限度符合FIPS 140-2“加密模块的安全要求”。 此外,SecureRandom必须产生非确定性输出。 因此,传递给SecureRandom对象的任何种子材料必须是不可预测的,并且所有SecureRandom输出序列必须具有加密强度。(官文,其实我也一知半解)

无论是否指定SecureRandom的初始seed,单个实例多次运行的结果也完全不同;多个不同的SecureRandom实例无论是否指定seed,即使指定一样的初始seed,同时运行的结果也完全不同。

SecureRandom继承自Random,但是对nextX方法中的底层方法进行的重写覆盖,不过仍然基于Random的CAS且SecureRandom的底层方法还使用的同步,所以在并发环境下,性能比Random差了一些。

本文转自https://www.iteye.com/blog/shift-alt-ctrl-2432102

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值