
为什么 Random 在高并发下会卡住
Random 的 nextInt() 看似线程安全,但底层靠 AtomicLong seed + CAS 自旋更新种子。当几十上百个线程同时调用,大部分线程会在 compareAndSet() 上失败重试,CPU 白耗在空转,吞吐量断崖下跌。
典型现象包括:JMC 或 Arthas 观察到大量线程处于 RUNNABLE 状态但实际没干活;压测时 QPS 上不去,ThreadLocalRandom.current().nextInt() 同等逻辑却能轻松翻倍。
不要复用同一个 Random 实例跨线程使用也不要为每个线程 new 一个 Random —— 虽然避开竞争,但构造开销大、种子初始值可能趋同(尤其系统时间精度不足时)更别用 synchronized(new Random()),锁粒度错、性能更差
ThreadLocalRandom.current() 必须每次调用,不能缓存静态实例
这是最容易踩的坑。有人图省事写成 private static final ThreadLocalRandom RANDOM = ThreadLocalRandom.current();,结果所有线程共享同一个初始化时刻的种子状态,后续生成的随机数序列完全一致。
原因在于:ThreadLocalRandom.current() 不是简单返回单例,而是检查当前线程是否已初始化种子 —— 未初始化则调用 localInit()(混合 System.nanoTime()、threadId 等生成强隔离种子);已初始化才返回本线程专属实例。
立即学习“Java免费学习笔记(深入)”;
✅ 正确写法:ThreadLocalRandom.current().nextInt(100),每次需要时都调用 current()❌ 错误写法:private static final ThreadLocalRandom R = ThreadLocalRandom.current();⚠️ 注意:在 ForkJoinPool 的子任务中同样适用,无需额外处理
边界参数行为与 Random 不完全兼容
ThreadLocalRandom 的 nextInt(int bound)、nextLong(long bound)、nextDouble(double bound) 都要求 bound > 0,否则抛 IllegalArgumentException;而 Random.nextInt(int bound) 同样如此,这点一致。
但注意区间语义:nextInt(50, 100) 表示 [50, 100),和 Random 完全相同;可一旦传入负数或 0,两者都立即失败,不静默降级。
不要假设 nextInt(-5) 会返回 0 或抛其他异常 —— 它一定抛 IllegalArgumentException并行流中直接用:IntStream.range(0, 1000).parallel().map(i -> ThreadLocalRandom.current().nextInt(100)).toArray(),安全且无额外同步若需复现某次随机序列(如测试),ThreadLocalRandom 不支持指定种子初始化,此时仍得用 new Random(seed) + 单线程上下文
ThreadLocalRandom 不是万能的,别在错误场景硬套
它解决的是「多线程高频调用随机数」这一具体问题。如果业务本身是单线程、或随机数调用频次极低(比如启动时生成一次密钥),用 Random 更直观,也无性能损失。
另外,它不提供 setSeed(long)、nextGaussian() 等方法 —— 这些能力被有意裁剪,因为线程局部场景下重置种子意义不大,而正态分布通常需更高精度控制,不在其设计目标内。
需要高斯分布?继续用 Random.nextGaussian(),或自己基于 nextDouble() 实现 Box-Muller需要可重现的测试随机流?坚持用 new Random(12345L),别试图给 ThreadLocalRandom “塞种子”它的优势只在“每毫秒数百次以上、多线程争抢”的场景里真正体现出来,别为低频调用过度设计

评论(0)