Random / SplittableRandom / SecureRandom / Base64 随机数与编码源码
概述
java.util 的随机数家族覆盖三种需求:Random(经典线程安全 LCG)、SplittableRandom(高性能、可分裂、非线程安全)、ThreadLocalRandom(每线程独立状态)、SecureRandom(密码学安全)。Base64 则是与随机数强相关的二进制编码工具——生成随机字节后常用它编码成可打印字符串。
两者的实现思路迥异:Random 靠 AtomicLong 种子 + 位运算保线程安全,SplittableRandom 靠 MurmurHash3 混合函数 + 分裂常数 获得高质量独立序列,Base64 则是纯位操作的分组编码。本文基于 OpenJDK 21 源码拆解这些算法细节。
核心源码解析
① Random.next(int bits) 的线性同余生成器
java
private static final long multiplier = 0x5DEECE66DL; // 乘数(Knuth 与 Lewis)
private static final long addend = 0xBL; // 加数
private static final long mask = (1L << 48) - 1; // 48 位掩码
private static final AtomicLong seedUniquifier = new AtomicLong(8682522807148012L);
public 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)); // ② CAS 保证线程安全
return (int)(nextseed >>> (48 - bits)); // ③ 取高 bits 位
}Random使用标准线性同余生成器(LCG):seed = (seed * 0x5DEECE66D + 0xB) mod 2^48,模数 2^48 用掩码实现。- 线程安全靠
AtomicLong种子加 CAS 循环:并发竞争时自旋重试,从不加锁,读多写少场景开销低。 - 输出取递推结果的高
bits位(>>> (48 - bits))——LCG 的低位周期更差,所以只暴露高位。 - 特性:完全确定(同种子同序列)、可预测,仅适合非安全场景;周期性 2^48 意味着大量生成后会重复。
② Random.nextInt(int bound) 的拒绝采样
java
public int nextInt(int bound) {
if (bound <= 0) throw new IllegalArgumentException("bound must be positive");
int r = next(31); // ① 取 31 位随机数
int m = bound - 1;
if ((bound & m) == 0) { // ② bound 是 2 的幂:直接掩码
r = (int)((bound * (long) r) >> 31);
} else {
for (int u = r; ; ) {
r = u % bound; // ③ 取模
if (u - r + (m) >= 0) break; // ④ 拒绝超出范围的顶部区间
u = next(31); // ⑤ 重取
}
}
return r;
}- 朴素的
next(31) % bound有模偏差:当 2^31 不能被 bound 整除时,余数较小的值出现概率更高。 - 拒绝采样消除偏差:
u - r + (m) >= 0判断u是否落在"均匀填充"的区间内,落在顶部残缺区间就重新取样。 bound为 2 的幂时走快速路径:(bound * r) >> 31等价于高位截取,无偏差且不需要循环。
③ SplittableRandom 的 gamma 分裂
java
private static final long GOLDEN_GAMMA = 0x9e3779b97f4a7c15L; // 黄金分割常数
public SplittableRandom split() {
return new SplittableRandom(nextLong()); // ① 以自身下一个随机数为新种子
}
// 构造器(内部)
private SplittableRandom(long seed, long gamma) {
this.seed = seed;
this.gamma = gamma; // ② 每个实例的 gamma 从构造器注入
}split()返回一个全新独立的SplittableRandom:新实例的种子取自己的下一个随机值,gamma由构造器传入(根实例用GOLDEN_GAMMA)。gamma必须是奇数(内部用Math.imul后置最低位保证),这是两路状态能长周期不碰撞的前提。- 设计动机:并行计算场景每个任务
split()一个实例,各实例互不共享状态,无需任何同步;GOLDEN_GAMMA是黄金比例的无理数近似,保证分裂出的序列错开。
④ SplittableRandom.nextInt() 的 MurmurHash3 混合
java
private int mix32(long z) {
z = (z ^ (z >>> 33)) * 0xff51afd7ed558ccdL; // ① 第一轮乘加
z = (z ^ (z >>> 33)) * 0xc4ceb9fe1a85ec53L; // ② 第二轮乘加
return (int)(z ^ (z >>> 33)); // ③ 雪崩
}
// 状态推进:下一状态 = 当前状态 * gamma
// 输出:mix32(状态)
private int internalNextInt() {
long r = mix64(seed); // 混合当前状态
seed = Math.imul(seed, gamma); // 状态按 gamma 乘性递推
return (int)(r >>> 32);
}- 状态推进用乘性递推
seed *= gamma(Math.imul取 64 位乘法低位),输出则对状态做 MurmurHash3 的 64 位雪崩混合。 - 雪崩混合(两轮"异或右移 33 + 乘大素数")把相邻状态映射到差异巨大的输出——即使状态序列相关性弱,输出也呈现均匀分布。
- 与
Random不同:不产出低质量低位,mix32直接给全 32 位质量均匀的输出,这也是它不需要"只取高位"的原因。
⑤ ThreadLocalRandom.current() 的线程本地随机
java
public static ThreadLocalRandom current() {
return U.getReference(Thread.currentThread(), THREADLOCALRANDOM); // ① 取线程字段
}
// ThreadLocalRandom 的生成不经过 seedUniquifier
final long nextSeed() {
Thread t; long r;
U.putLong(t = Thread.currentThread(), SEED,
r = U.getLong(t, SEED) + GAMMA); // ② 线程内字段直接加 GAMMA
return r;
}ThreadLocalRandom把种子放在Thread对象的threadLocalRandomSeed字段里,通过Unsafe直接读写,current()只是取线程字段引用——比ThreadLocal的存储还快一档。- 推进方式与
SplittableRandom相同:seed += GAMMA(GAMMA是Math.imul(-GOLDEN_GAMMA, ...)调整出的奇数),混合后输出。 - 每个线程独立状态,完全无锁无 CAS;代价是实例不能跨线程共享(文档明确:不要存储到字段中供多线程使用)。
⑥ SecureRandom 的 NativePRNG / SHA1PRNG
java
// sun.security.provider.NativePRNG(Unix)
protected void engineNextBytes(byte[] bytes) {
...
int len = bytes.length;
if (len <= 0) return;
...
nativeGenerateSeed(bytes); // ① 从 /dev/random 或 /dev/urandom 读取
}SecureRandom走Provider.Service创建SecureRandomSpi实例;Linux 常见实现NativePRNG:engineNextBytes从操作系统熵源(/dev/random或/dev/urandom)读随机字节,或经getrandom系统调用。- 替代实现
SHA1PRNG:以系统熵为种子,用 SHA-1 内部状态递推输出伪随机字节——纯 Java 实现,用于不支持 native 熵源的平台;强度弱于 native 熵源。 - 关键差异:密码学安全(不可预测、种子不泄露),适用于密钥、Token、盐;
Random/SplittableRandom的种子可被推断,绝不能用于安全场景。 SecureRandom.getInstanceStrong()强制要求高强度的熵源实现,供密钥生成等场景使用。
⑦ Base64.getEncoder().encodeToString(byte[] src) 的编码
java
public String encodeToString(byte[] src) {
byte[] encoded = encode(src); // ① 编码为字节数组
return new String(encoded, 0, 0, encoded.length); // ② 按 ASCII 解码成字符串
}
private void encode0(byte[] src, int off, int end, byte[] dst) {
...
while (sp < sl) {
int b0 = src[sp++] & 0xff;
int b1 = src[sp++] & 0xff;
int b2 = src[sp++] & 0xff;
int bits = (b0 << 16) | (b1 << 8) | b2; // ③ 24 位拼装
dst[dp++] = base64[bits >>> 18 & 0x3f]; // ④ 每 6 位一个字符
dst[dp++] = base64[bits >>> 12 & 0x3f];
dst[dp++] = base64[bits >>> 6 & 0x3f];
dst[dp++] = base64[bits & 0x3f];
}
}- Base64 把每 3 字节(24 位)拆成 4 个 6 位组,每组查
base64[]映射表(A-Z a-z 0-9 + /)输出 1 个字符,3 字节 → 4 字符。 - 尾部长 1/2 字节时补零并用
=填充:剩余 1 字节 → 2 字符 + 2 个=;剩余 2 字节 → 3 字符 + 1 个=。 - 内部
encode0是纯位运算循环(位移 + 掩码查表),无任何分支开销,性能关键路径;编码结果直接new String复用字节数组(byte[]构造不走拷贝)。
⑧ Base64.Encoder.withoutPadding() 的省略填充
java
// 内部:构造时标记是否需要填充
Base64.Encoder(boolean isURL, byte[] newline, int linemax, boolean doPadding) {
this.isURL = isURL;
this.newline = newline;
this.linemax = linemax;
this.doPadding = doPadding; // ① 是否输出 '=' 填充
}
public Encoder withoutPadding() {
if (!doPadding) return this; // ② 已无填充则返回自身
return new Encoder(isURL, newline, linemax, false); // ③ 新建无填充实例
}withoutPadding()返回一个新的Encoder(doPadding = false),末尾的=不再输出。- 典型用途:JWT 的 URL 段、OAuth token——它们的 Base64 段约定省略填充,解码时按长度补
=即可还原。 Encoder是不可变且线程安全的:所有字段 final,一次配置终身复用;getEncoder()等工厂返回的静态单例可在多线程共享。
⑨ Base64.URLSafe vs Base64.Basic
java
public static Encoder getEncoder() {
return Encoder.RFC4648; // ① Basic:标准字母表
}
public static Encoder getUrlEncoder() {
return Encoder.RFC4648_URLSAFE; // ② URLSafe:换字母表
}- 字母表差异:
Basic用+与/;URLSafe用-与_(替换掉 URL/文件名中不安全的字符)。 - 换行差异:
Basic的getEncoder()输出 76 字符一换行(MIME兼容风格,linemax = 76);URLSafe无换行(linemax = -1)。 - 两者填充规则一致(默认
=填充,可withoutPadding()关闭);解码时Basic的getDecoder()接受含换行的输入并忽略,URLSafe解码器只认-/_字母表。 - 选型提示:URL 查询参数、文件名用
getUrlEncoder();标准协议/存储用getEncoder();要求紧凑且无分隔符时getEncoder().withoutPadding()。