Java Random类之深入浅出

1 篇文章 1 订阅
1 篇文章 0 订阅

Random


1.线性同余实现伪随机

程序员对随机数一般都不陌生,而且众所周知,计算机中通常实现的是伪随机数列。何为伪随机数列?

伪随机数(或称伪乱数),是使用一个确定性的算法计算出来的似乎是随机的数序,因此伪随机数实际上并不随机。

既然是通过算法来模拟随机过程,那什么样的算法可以达到接近随机的效果?

比较简单的一种便是线性同余法
在这里插入图片描述
其中 A 称为乘数,B 称为增量,M 称为模数,当 A=0,C≠0 时称为和同余法,当 C=0,A≠0 时称为乘同余法,A≠0,C≠0 时,称为混合同余法。乘数、增量和模数的选取可以多种多样,只要保证产生的随机数有较好的均匀性和随机性即可,一般采用 m=2km=2k 混合同余法。

用线性同余法产生随机数的特点是非常容易实现,生成速度快,但是弊端也很明显,32位的数周期最长只能到2的32次方,达不到需要高质量随机数的应用如加密应用的要求



2.JDK中伪随机数生成

很多平台都采用以上线性同余发生器实现了伪随机数的生成机制,比如下面是常见的平台实现中对应的参数(from wiki):
在这里插入图片描述

代码演示如下:

public class LinearCongruentialGenerator {
    final static int mask = (1 << 31) - 1;

    static IntStream randBSD(int seed) {
        return iterate(seed, s -> (s * 1_103_515_245 + 12_345) & mask).skip(1);
    }

    static IntStream randMS(int seed) {
        return iterate(seed, s -> (s * 214_013 + 2_531_011) & mask).skip(1)
                .map(i -> i >> 16);
    }

    public static void main(String[] args) {
        System.out.println("BSD:");
        randBSD(0).limit(10).forEach(System.out::println);

        System.out.println("\nMS:");
        randMS(0).limit(10).forEach(System.out::println);
    }
}

java 中 java.util.Random类的实现中,发生器的关键代码如下:

private static final long multiplier = 0x5DEECE66DL;
private static final long addend = 0xBL;
private static final long mask = (1L << 48) - 1;


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));//丢弃低比特位,保留高比特位
}

public int nextInt() {
    return next(32);
}
public long nextLong() {
    // it's okay that the bottom word remains signed.
    return ((long)(next(32)) << 32) + next(32);
}

同时可以看到,上面实现中特意对比特位进行了截取,丢弃低比特位,保留高比特位。



3.Random类


Random简介

在 JDK 的Random 类中,官方文档有如下介绍和建议:

  • 该类的一个实例用于生成伪随机数流。 类使用一个 48 位种子,该种子使用一个线性同余公式进行修改。
    (参见Donald Knuth,《计算机编程的艺术》,第二卷,第3.2.1节。)

  • 如果使用相同的种子创建了两个 Random 实例,并且对每个实例进行了相同的方法调用序列,则它们将生成并返回相同的数字序列。为了保证此属性,为 Random 类指定了特定的算法。为了实现 Java 代码的绝对可移植性,Java实现必须将此处显示的所有算法用于 Random 类。但是,Random 类的子类允许使用其他算法,只要它们遵守所有方法的常规协定即可。

  • 由 Random 类实现的算法使用 protected int next(int bits) 作为核心实用方法,该方法在每次调用时最多可以提供32位的伪随机生成数。

  • java.util.Random的实例是线程安全的。但是,跨线程并发使用同一 java.util.Random实例可能会引起争用并因此导致性能下降。多线程设计中建议使用{java.util.concurrent.ThreadLocalRandom}。

  • java.util.Random 的实例不是加密安全的。考虑改为使用 java.security.SecureRandom 来获取加密安全的伪随机数生成器,以供对安全敏感的应用程序使用。


Random构造方法

Random 类有两个构造方法:

Random()
Random(long seed)

先来说一下 Random(long seed),源码如下(便于理解,翻译了注释):

/**
 * 使用单个 long类型的种子创建一个新的随机数生成器。
 * seed是伪随机数生成器的内部状态的初始值,由 next方法维护。
 * 此方法等价于:
 *      Random rnd = new Random();
 *      rnd.setSeed(seed);
 *      
 * @param seed 最初的种子
 */
public Random(long seed) {
    /*
     * 如果使用的是 Random 本类, 则调用 initialScramble方法
     * 对 seed 进行初始混乱(可以看出,采用的是混合同余法)
     */
    if (getClass() == Random.class)
        this.seed = new AtomicLong(initialScramble(seed));
    else {
        /*
         * 否则,则是 Random 的子类,子类有可能会重写setSeed方法,
         * 因此,调用 setSeed 方法对 seed 进行操作,而不是调用本类
         * 的混乱方法。
         */
        this.seed = new AtomicLong();
        setSeed(seed);
    }
}

这种重载形式的构造方法,由用户提供种子(seed),但一般不建议这么做,因为种子会影响随机分布的质量和区域。而且指的注意的事,如果使用相同的种子创建了两个 Random 实例,并且对每个实例进行了相同的方法调用序列,则它们将生成并返回相同的数字序列

用代码炒个栗子:

Random r1 = new Random(996);
Random r2 = new Random(996);
for (int i = 0; i < 6; i++) {
    System.out.println(r1.nextInt() == r2.nextInt());
}

结果全为 true,即如果使用相同的种子创建了两个 Random 实例,两个实例同时调用第N次且调用的是相同方法,则生成的结果相同。

再来看一下 Random(),源码如下:

public Random() {
    this(seedUniquifier() ^ System.nanoTime());
}

可以看到,其内部还是调用了 Random(long seed) 的重载形式,只不过这时的 种子(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;
    }
}

private static final AtomicLong seedUniquifier = new AtomicLong(8682522807148012L);

此时 seed 是由 Random类自行维护,其 seed 的初始值为 8682522807148012L * 181783497276652981L 再异或当前系统纳秒时间。因此几乎不可能生成两个 seed 值相同的 Random 实例,也就几乎不会出现生成相同结果的情况(除非巧合)。


Random的常用方法

方法介绍
void setSeed(long seed)设置 seed(种子)
int nextInt()随机生成一个 int 值,范围:[Integer.MIN_VALUE,Integer.MAX_VALUE]
int nextInt(int bound)随机生成一个 int 值,范围:[0,bound-1]
long nextLong()随机生成一个 long 值,范围:[Long.MIN_VALUE,Long.MAX_VALUE]
float nextFloat()随机生成一个 float 值,范围:[0,1]
double nextDouble()随机生成一个 double 值,范围:[0,1]
double nextGaussian()正态分布生成一个 double 值的伪随机数,平均值为 0.0,生成器序列标准差为1.0
void nextBytes(byte[] bytes)随机生成 bytes.length 个 byte 值,并填充到 bytes 数组中,每个值的范围:[Byte.MIN_VALUE,Byte.MAX_VALUE]
boolean nextBoolean()随机生成一个 boolean 值,true | false
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值