今日思考:程序是如何实现随机的?以及什么是真随机和伪随机
1. 背景
今日神游,忽然沉思:随机是指现实生活中无规律,不可预测的事情,例如:抛硬币。那么在软件的世界里是如何实现这一物理现象的呢,遂做此探究。。。
什么是真随机和伪随机?
要弄明白程序是怎么实现随机的,首先得明白两个概念:
- 真随机
- 伪随机
2. 伪随机(Pseudo-Random)
2.1 定义
伪随机数(Pseudo-Random Number, PRN)是通过确定性算法生成的,看似随机但可预测的数列。这类随机数由数学公式计算得出,如果使用相同的初始种子(seed),则会生成相同的随机数序列。
2.2 伪随机的特点
- 可复现性(Deterministic):相同的种子(seed)会生成相同的随机数序列。
- 速度快(Fast):伪随机数使用数学公式生成,比真随机数快。
- 周期性(Periodic):大多数伪随机数生成器最终会循环重复数值。
2.3 伪随机的常见算法
线性同余生成器(Linear Congruential Generator, LCG)
( X_{n+1} = (aX_n + c) \mod m )
Java 的Random
类使用 LCG 算法。梅森旋转算法(Mersenne Twister, MT19937)
Python 的random
模块使用 MT19937,周期长((2^{19937}-1)),适用于科学计算和游戏随机数。XOR-Shift 算法
java.util.concurrent.ThreadLocalRandom
采用此算法,适用于多线程环境。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 常见的真随机数来源
-
硬件随机数生成器(Hardware RNG, HRNG)
- Intel CPU 的
RDRAND
指令 - AMD CPU 的
RDSEED
指令 - 硬件安全模块(HSM, Hardware Security Module)
- Intel CPU 的
-
操作系统熵池(Entropy Pool)
-
Linux:
/dev/random
(熵不足时会阻塞)、/dev/urandom
(非阻塞) -
Windows:
CryptGenRandom
API
-
Linux:
-
用户行为
- 采集用户鼠标移动、键盘输入时间间隔
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
类的默认seed
是System.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
-
Windows:
CryptGenRandom
API
问题: 如何确保 SecureRandom
使用真随机
SecureRandom secureRandom = SecureRandom.getInstanceStrong();
强制使用真随机源:
SecureRandom secureRandom = SecureRandom.getInstance("NativePRNGBlocking");
-
NativePRNGBlocking
使用/dev/random
,但可能会阻塞。 -
NativePRNGNonBlocking
使用/dev/urandom
,速度快,但可能不是完全真随机。
最后
以上就是今日的一些思考,希望对你有帮助。
欢迎大家关注gzh: 加瓦点灯, 每天推送有意思的知识!
本文由mdnice多平台发布