在日常开发中,随机数生成是一个再常见不过的需求。但面对 Random、ThreadLocalRandom、SecureRandom 这些类,你真的选对了吗?
这篇文章不罗列 API,而是从底层原理出发,帮你理清它们之间的本质差异,以及各自的最佳适用场景。
一、Random vs ThreadLocalRandom:并发性能差在哪?
1.1 两者算法完全一致
先明确一点:ThreadLocalRandom 是 Random 的"线程本地化"版本,它们的随机数生成算法完全一样 ------都是线性同余生成器(LCG)。
java
// Random 的种子更新
nextseed = (oldseed * 25214903917L + 11L) & ((1L << 48) - 1);
// ThreadLocalRandom 的种子更新(完全一致)
r = (r * 25214903917L + 11L) & ((1L << 48) - 1);
随机质量没有任何区别,差异只在于多线程下的性能表现。
1.2 Random 的性能瓶颈:CAS 自旋与总线锁
Random 通过 CAS(Compare-And-Swap)保证线程安全。所有线程共享一个 AtomicLong 类型的种子,每次生成随机数都要 CAS 更新这个种子。
java
// Random.nextInt() 的核心逻辑(简化)
do {
oldseed = seed.get();
nextseed = (oldseed * multiplier + addend) & mask;
} while (!seed.compareAndSet(oldseed, nextseed)); // CAS 自旋
问题出在高并发场景:
-
大量线程同时 CAS 更新同一个种子,绝大多数会失败
-
失败的线程不会阻塞,而是在循环中不断重试(自旋)
-
每个 CAS 操作在 CPU 层面会触发
LOCK前缀指令,锁定内存总线,导致所有核心的内存访问串行化 -
结果:CPU 利用率飙升至 100%,但大部分算力浪费在无意义的重试上,系统吞吐量急剧下降
这就是 CAS 的经典缺陷------适合低竞争,不适合高竞争。
1.3 ThreadLocalRandom 的解决方案:空间换时间
ThreadLocalRandom 为每个线程独立维护一份种子,线程之间完全隔离,不存在任何共享资源。
-
没有 CAS,没有自旋
-
没有总线锁
-
种子更新是普通的 CPU 本地写操作
性能对比数据(来自 JMH 基准测试):
| 线程数 | Random 耗时 (ms) |
ThreadLocalRandom 耗时 (ms) |
|---|---|---|
| 50 | 1192 | 575 |
| 100 | 4031 | 162 |
| 500 | 11311 | 97 |
| 1000 | 23418 | 174 |
Random 的性能随线程数增加急剧恶化,而 ThreadLocalRandom 几乎保持恒定。
1.4 一个常见的误区
"每次调用时
new Random(),每个线程都有自己的实例,就不会有竞争了吧?"
这是错的。 即使每个线程持有独立的 Random 对象,每个对象内部的 AtomicLong 种子在 CAS 更新时,仍然会在 CPU 总线层面产生串行化排队。再加上频繁创建对象带来的 GC 压力,高并发下性能依然堪忧。
二、LCG 的先天缺陷:Random 和 ThreadLocalRandom 的通病
LCG(线性同余生成器)是 Random 和 ThreadLocalRandom 共同采用的算法,它有几个无法回避的缺陷:
2.1 周期有限
LCG 的周期最多为 2⁴⁸(Java 实现),生成完这个数量的随机数后序列必然重复。对于大多数业务场景足够了,但科学计算中可能不够。
2.2 低位随机性极差
LCG 的低位比特循环周期极短------最低位只有 2 (0、1、0、1......交替)。这也是 Random.nextInt() 只取高 31 位、跳过最低位的原因。
2.3 可预测性
LCG 是确定性算法,状态空间仅 48 位。攻击者通过连续几个输出值就能反推出种子,进而预测后续所有随机数。
java
// 不要用 Random 生成敏感数据
String token = new Random().nextInt(1000000) + ""; // 危险!
2.4 高维分布不均匀
LCG 在三维及以上空间中,所有点会落在少数平行的超平面上,分布极不均匀。这在蒙特卡洛模拟等场景下会导致结果失真。
三、SecureRandom:安全的代价
3.1 它解决了什么问题?
SecureRandom 使用真正的熵源 (如系统中断、硬件随机数发生器、/dev/random)生成种子,输出不可预测,不存在 LCG 的周期性和可预测性问题。
3.2 它的性能问题
SecureRandom 不使用 CAS ,其线程安全依赖 synchronized 同步锁或特定实现的本地策略。
-
默认实现采用
synchronized,高并发下线程会阻塞等待 -
某些实现(如
NativePRNG)会读取/dev/random,可能产生 I/O 阻塞
数据佐证 (OpenJDK 社区测试):8 个线程并发时,NativePRNG 的性能下降至单线程的约 60%。
3.3 高并发下的优化手法
如果既要安全性又要高吞吐量,可以使用 ThreadLocal<SecureRandom> 为每个线程隔离实例,避免锁竞争:
java
private static final ThreadLocal<SecureRandom> SECURE_RANDOM_HOLDER =
ThreadLocal.withInitial(SecureRandom::new);
四、选型决策树
text
你的场景是什么?
│
├── 安全敏感(密码、token、SessionID、加密密钥)
│ └── 必须用 SecureRandom
│
├── 高并发业务(Web 接口、RPC 服务、高吞吐量系统)
│ ├── 对随机质量要求不高(抽奖、随机分配、流量染色)
│ │ └── ThreadLocalRandom(最优)
│ └── 对随机质量要求高(科学计算、蒙特卡洛模拟)
│ └── SplittableRandom(算法更优)或自行 ThreadLocal<SecureRandom>
│
└── 单线程 / 低并发(批处理、管理后台、低频任务)
└── Random 或 ThreadLocalRandom 都行,Random 更简洁
五、一句话总结
| 类 | 并发性能 | 随机质量 | 适用场景 |
|---|---|---|---|
Random |
低并发 OK,高并发差 | LCG,一般够用 | 单线程/低并发 |
ThreadLocalRandom |
高并发最优 | LCG,一般够用 | 所有多线程非安全场景 |
SecureRandom |
较差(同步锁/I/O阻塞) | 不可预测,安全 | 密码、token、加密 |
SplittableRandom |
好 | 算法优于 LCG | 科学计算、并行流 |
实践建议:
-
一般业务开发 :无脑用
ThreadLocalRandom,简单、高性能、线程安全,覆盖 90% 的场景 -
安全相关 :必须上
SecureRandom,性能差点可以接受,安全不能妥协 -
科学计算 :换
SplittableRandom,随机质量更高
选对工具,既不是"无脑用最优",也不是"永远用最安全",而是理解原理后,在性能、安全、代码简洁之间做出合理的取舍。
希望这篇文章能帮你彻底理清 Java 随机数生成的选型思路。如果觉得有用,欢迎分享给需要的朋友。