深度剖析 Java Random生成随机数!

举报
猿java 发表于 2024/11/30 10:39:49 2024/11/30
【摘要】 你好,我是猿java在 JDK的java.util包里提供了一个用于生成随机数的Random类,它是如何生成随机数的?为什么它生成的随机数是均匀的?今天我们一起来聊聊其背后的原理。本文基于Java语言,jdk 11 java.util.RandomRandom是java.util包提供的一个用于生成随机数的类,首先,我们看看官方对它的描述:通过源码,我们总结出几个核心点:Random类的实例...

你好,我是猿java

在 JDK的java.util包里提供了一个用于生成随机数的Random类,它是如何生成随机数的?为什么它生成的随机数是均匀的?今天我们一起来聊聊其背后的原理。

本文基于Java语言,jdk 11

java.util.Random

Randomjava.util包提供的一个用于生成随机数的类,首先,我们看看官方对它的描述:

java-util-random.png

通过源码,我们总结出几个核心点:

  • Random类的实例是用来生成一系列的伪随机数;
  • Random类使用一个 48位的种子(seed),通过线性同余算法进行修改;
  • Random类的特定算法被指定,所以,两个Random类的实例使用相同的种子创建,并且对于每个实例都调用相同顺序的方法,它们将生成并返回相同的数字序列
  • Random类是线程安全的,但是,跨线程同时使用同一个java.util.Random实例可能会遇到竞争和相应的性能问题;
  • 在多线程设计中,考虑使用java.util.concurrent.ThreadLocalRandom;
  • Random类的实例不是密码安全的,对于安全敏感的应用程序,考虑使用java.security.SecureRandom;

什么是伪随机数?

伪随机数指的是一种看起来像随机数的序列,但实际上是由确定性算法生成的。这种算法称为伪随机数生成器(PRNG,Pseudo-Random Number
Generator)。
PRNG使用一个称为"种子"的初始值,然后通过一系列的数学运算来生成一个序列,这个序列看起来具有随机性的特征,比如均匀分布、无序性等。

什么是种子(seed)?

在随机数生成器中,种子(seed)其实就是一个起始值,它用于初始化随机数生成器的状态。随机数生成器使用这个种子来确定生成随机数的序列。种子决定了随机数生成器的初始状态,因此给定相同的种子,将会生成相同的随机数序列。

线性同余算法

线性同余算法(LCG,Linear Congruential Generator)是最基本的伪随机数生成算法之一,该算法通常使用如下方程表示:

𝑋𝑛+1 = (a * 𝑋𝑛 + c) mod m

其中:

  • 𝑋𝑛 是当前的随机数
  • 𝑋𝑛+1 是下一个随机数
  • a、c 和 m 是事先选定的常数
  • a、c 和 m 是正整数

为了更好地理解这个方程,我们通过一个具体的例子来进行说明:

假设:a = 4, c = 1, m = 7X= 3,即种子 seed = 3
则:
   X= (4 * X+ 1) mod 5 = (4 * 3 + 1) mod 7 = 6
   X= (4 * X+ 1) mod 5 = (4 * 6 + 1) mod 7 = 4
   X= (4 * X+ 1) mod 5 = (4 * 4 + 1) mod 7 = 3
   X= (4 * X+ 1) mod 5 = (4 * 3 + 1) mod 7 = 6 (6,4,3)循环开始
   X= (4 * X+ 1) mod 5 = (4 * 6 + 1) mod 7 = 4
   X= (4 * X+ 1) mod 5 = (4 * 4 + 1) mod 7 = 3
   ... 

说明:mod是取余操作,等同于 %

通过上面的示例可以看出:如果我们设定一个种子seed = 3,后面每一次获取随机数都可以通过该方程计算出来,而且按照(6,4,3)
这个周期进行循环,整个过程获取的数字看起来是随机的,实际上又是通过固定的方法计算而来,因此叫做伪随机数

对于线性同余算法,需要重点考虑以下 5个因素:

  1. 种子(Seed): 线性同余算法的运行依赖于一个种子,改变该种子会产生不同的随机数序列,但给定相同的种子和参数,将会生成相同的序列。
  2. 数选择参: a、c 和 m 的选择是至关重要的,不同的参数会导致不同质量的随机数序列,包括周期长度、统计特性等。
  3. 周期性: 线性同余算法生成随机数序列是周期性的,通过上面的例子也可以看出。
  4. 统计特性: 线性同余算法的生成的随机数序列可能不满足一些统计特性,如均匀分布、独立性等。
  5. 效率: 线性同余算法是一种非常高效的随机数生成算法,因为它只涉及简单的数学运算。这使得它在许多情况下都是一个合适的选择,尤其是对于需要大量随机数的应用。

好了,理解了线性同余算法的实现原理,接下来我们来分析Random是如何计算随机数。

Random生成随机数

Random类包含两个构造方法,如下:

// 无参构造器
public Random() {
    this(seedUniquifier() ^ System.nanoTime());
}

// 接收一个 seed参数的构造器
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);
    }
}

// initialScramble方法是用于对种子进行初始的混淆处理,以增加生成的随机数的随机性
private static long initialScramble(long seed) {
    return (seed ^ multiplier) & mask;
}

当使用Random的无参构造器时,Random内部会生成一个seed,生成方式如下:

seed = current * 1181783497276652981L ^ System.nanoTime()

如果 current没有设置,默认的初始值是:1181783497276652981L。然后,用新生成的seed再调用带参构造方法,构造器内部有initialScramble(long seed)方法,用于对种子进行初始的混淆处理,以增加生成的随机数的随机性,保证了其均匀性。

为了更好的说明java.util.Random是如何生成随机数,这里以其nextDouble()为例进行讲解,其源码如下:


public double nextDouble() {
    return (((long) (next(26)) << 27) + next(27)) * DOUBLE_UNIT;
}

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

nextDouble()方法用于生成一个介于[0, 1.0)之间的随机数,nextDouble()方法可以体现出Random对线性同余算法的具体实现如下:

线性同余算法:𝑋𝑛+1 = (a * 𝑋𝑛 + c) mod m 
Random的具体实现:(seed ^ 0x5DEECE66DL) & ((1L << 48) - 1)

其中 a, c, m都是指定的值,分别为:

  • a 是 0x5DEECE66DL(16进制),转换成 10进制为25214903917,
  • c 是 11(0xB),
  • m 是 2⁴⁸,即 281474976710656(10进制) 或 0x1000000000000L(16进制)。

另外,java.util.Random还包含另外一些常用的方法,如下:

public int nextInt() {
    return next(32);
}

public boolean nextBoolean() {
    return next(1) != 0;
}

public void nextBytes(byte[] bytes) {
    for (int i = 0, len = bytes.length; i < len; )
        for (int rnd = nextInt(),
             n = Math.min(len - i, Integer.SIZE / Byte.SIZE);
             n-- > 0; rnd >>= Byte.SIZE)
            bytes[i++] = (byte) rnd;
}

Math.random()

Math.random()方法是日常开发中生成随机数使用最多的方法,其本质是对Random类的包装,下面为 Math.random()的源码实现:

public static double random() {
    return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
}

private static final class RandomNumberGeneratorHolder {
    static final Random randomNumberGenerator = new Random();
}

通过Math.random()的源码可以发现:Math.random() 的实现其实就是 (new Random()).nextDouble(),在这里就不赘述了,另外,日常开发中对Math.random()对真实使用方式,这里也以一副手稿来总结:

math-random.png

解释:

  • Math.random()生成一个介于[0, 1)的随机数字,即 0~0.999…
  • Math.random() * 8生成一个介于[0, 8)的随机数字,即 0~7.999…

到此,Random生成随机数讲解完成,下面我们进行总结:

总结

上述我们分析了几种常见的随机数生成方式,具体选用哪种可以根据自身业务:

  • 线性同余算法是一种生成随机数的常用方法,其实现方程为𝑋𝑛+1 = (a * 𝑋𝑛 + c) mod m;
  • java.util.Random是 JDK提供的一种随机数生成类,其核心算法就是线性同余算法;
  • Math.random() 本质上是对 java.util.Random的包装;

参考资料

字母及数字上标下标

Using Math.random in Java

交流学习

如果文章存在缺点和错误,欢迎批评指正。更多干货和面试经,关注公众号:猿java。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。