今日思考:程序是如何实现随机的?以及什么是真随机和伪随机

1. 背景

今日神游,忽然沉思:随机是指现实生活中无规律,不可预测的事情,例如:抛硬币。那么在软件的世界里是如何实现这一物理现象的呢,遂做此探究。。。

什么是真随机和伪随机?

要弄明白程序是怎么实现随机的,首先得明白两个概念:

  1. 真随机
  2. 伪随机

    2. 伪随机(Pseudo-Random)

2.1 定义

伪随机数(Pseudo-Random Number, PRN)是通过确定性算法生成的,看似随机但可预测的数列。这类随机数由数学公式计算得出,如果使用相同的初始种子(seed),则会生成相同的随机数序列。

2.2 伪随机的特点

  • 可复现性(Deterministic):相同的种子(seed)会生成相同的随机数序列。
  • 速度快(Fast):伪随机数使用数学公式生成,比真随机数快。
  • 周期性(Periodic):大多数伪随机数生成器最终会循环重复数值。

2.3 伪随机的常见算法

  1. 线性同余生成器(Linear Congruential Generator, LCG)
    ( X_{n+1} = (aX_n + c) \mod m )
    Java 的 Random 类使用 LCG 算法。
  2. 梅森旋转算法(Mersenne Twister, MT19937)
    Python 的 random 模块使用 MT19937,周期长((2^{19937}-1)),适用于科学计算和游戏随机数。
  3. XOR-Shift 算法
    java.util.concurrent.ThreadLocalRandom 采用此算法,适用于多线程环境。
  4. PCG(Permuted Congruential Generator)
    近年提出的高质量伪随机数算法,周期长且统计性质优良。

3. 真随机(True Random)

3.1 定义

真随机数(True Random Number, TRN)依赖物理现象产生,例如:

  • 电子噪声(Thermal Noise)
  • 放射性衰变(Radioactive Decay)
  • 用户输入时间间隔(Keyboard/Mouse Timing)
  • 硬盘磁头寻道时间(Disk Seek Time)
  • 网络封包延迟(Network Latency)

3.2 真随机的特点

  • 不可预测(Non-deterministic):无法通过数学公式计算得出。
  • 无周期性(Aperiodic):不会出现循环模式。
  • 通常较慢(Slower):依赖外部环境采样,比纯数学计算慢。
  • 可能受环境影响(Environmental Influence):例如噪声水平变化可能影响随机性。

3.3 常见的真随机数来源

  1. 硬件随机数生成器(Hardware RNG, HRNG)

    • Intel CPU 的 RDRAND 指令
    • AMD CPU 的 RDSEED 指令
    • 硬件安全模块(HSM, Hardware Security Module)
  2. 操作系统熵池(Entropy Pool)

    • Linux/dev/random(熵不足时会阻塞)、/dev/urandom(非阻塞)
    • WindowsCryptGenRandom API
  3. 用户行为

    • 采集用户鼠标移动、键盘输入时间间隔

4. 真随机 vs. 伪随机对比

特性伪随机(Pseudo-Random)真随机(True Random)
定义计算机算法生成的可预测随机数依赖物理现象生成的不可预测随机数
是否可复现
是否可预测
生成速度较慢
是否有周期性
是否适用于密码学

程序是如何实现随机的?

上述概念我们其实可以得知,真随机一定要依赖现实世界的物理现象,因此,如果是纯软件实现的随机函数,都是伪随机。如果软件代码依赖了硬件(例如:硬件噪声)那么就可以实现真随机。

Java语言实现真随机和伪随机代码示例

伪随机

import java.util.Random;

public class PseudoRandomExample {
    public static void main(String[] args) {
        Random random = new Random(12345);  // 固定种子
        System.out.println(random.nextInt(100));  // 生成 0~99 之间的随机数
        System.out.println(random.nextInt(100));  
        System.out.println(random.nextInt(100));  
    }
}

输出(每次运行相同):

51
35
86

原理

Java 的 java.util.Random 类就是一个典型的伪随机数生成器(PRNG),它使用线性同余生成器(Linear Congruential Generator, LCG)算法。

线性同余算法(LCG)**

LCG 公式如下:
[
X_{n+1} = (aX_n + c) \mod m
]

  • (X_n) 是当前的随机数(或称“种子”)
  • (X_{n+1}) 是下一个随机数
  • (a)(乘数)、(c)(增量)、(m)(模数)是预定义的常数
  • Random 类的默认 seedSystem.nanoTime() ^ multiplier
Random 类的源码解析

Random 主要依赖 next() 方法,每次调用 nextInt()nextLong() 等方法时,都会基于 LCG 计算新的伪随机数。

private final AtomicLong seed;
private static final long MULTIPLIER = 0x5DEECE66DL;
private static final long ADDEND = 0xBL;
private static final long MASK = (1L << 48) - 1;
  • MULTIPLIER = 0x5DEECE66DL(乘数)
  • ADDEND = 0xBL(增量)
  • MASK = (1L << 48) - 1(模数,保证种子始终在 48 位范围)
核心的 next() 方法
protected int next(int bits) {
    long oldSeed, nextSeed;
    AtomicLong seed = this.seed;
    do {
        oldSeed = seed.get();
        nextSeed = (oldSeed * MULTIPLIER + ADDEND) & MASK;  // LCG 公式
    } while (!seed.compareAndSet(oldSeed, nextSeed));
    return (int) (nextSeed >>> (48 - bits));  // 取高 bits 位作为随机数
}
  • 采用 LCG 公式更新 seed
  • 使用 compareAndSet 保证线程安全
  • 右移 48-bits 以返回所需的位数
nextInt() 实现
public int nextInt() {
    return next(32);
}
  • nextInt() 直接调用 next(32),返回 32 位整数
  • next() 方法返回 48-bits,但 nextInt() 只取高 32 位
nextInt(int bound)(带上界)
public int nextInt(int bound) {
    if (bound <= 0)
        throw new IllegalArgumentException("bound must be positive");

    int r, m = bound - 1;
    if ((bound & m) == 0)  // 如果 bound 是 2 的幂
        return (int) ((bound * (long) next(31)) >> 31);

    do {
        r = next(31);
    } while (r - (r % bound) + m < 0);  // 确保均匀分布
    return r % bound;
}
  • 如果 bound 是 2 的幂,可以直接用位运算优化
  • 如果 bound 不是 2 的幂,用 拒绝采样法(Rejection Sampling) 处理边界情况,保证均匀分布

真随机

import java.security.SecureRandom;

public class TrueRandomExample {
    public static void main(String[] args) {
        SecureRandom secureRandom = new SecureRandom();
        System.out.println(secureRandom.nextInt(100));  // 生成 0~99 之间的真随机数
    }
}

问题: SecureRandom 真的是真随机吗?

其实,即便使用了SecureRandom , 也不一定是真随机,它的随机性取决于底层的实现。

SecureRandom 默认是加密安全的伪随机数生成器(CSPRNG)。它通常使用操作系统的安全随机源,如:

  • Linux/macOS/dev/urandom/dev/random
  • WindowsCryptGenRandom API

问题: 如何确保 SecureRandom 使用真随机

SecureRandom secureRandom = SecureRandom.getInstanceStrong();

强制使用真随机源:

SecureRandom secureRandom = SecureRandom.getInstance("NativePRNGBlocking");
  • NativePRNGBlocking 使用 /dev/random,但可能会阻塞。
  • NativePRNGNonBlocking 使用 /dev/urandom,速度快,但可能不是完全真随机。

最后

以上就是今日的一些思考,希望对你有帮助。

欢迎大家关注gzh: 加瓦点灯, 每天推送有意思的知识!

本文由mdnice多平台发布


加瓦点灯
0 声望0 粉丝

北漂后端程序员