在Java中生成随机数最常见的两个类是java.util.Random和java.util.concurrent.ThreadLocalRandom。许多人在写工具类时随手new一个Random丢进全局变量,多线程一跑就会发现CPU占用偏高,甚至偶发数值碰撞。其实这两者的设计目标完全不同:Random偏向通用、单线程友好;ThreadLocalRandom则是为高并发场景专门优化的线程本地随机数实现。下面我们通过原理、用法和性能三个层面把它们的正确打开方式讲清楚。

Random的底层原理与基础用法
Random内部维护一个48位的原子种子(seed),每次调用nextInt()、nextDouble()等方法时,都会通过线性同余算法更新种子,再根据新种子计算出伪随机值。为了保证多线程安全,Random使用AtomicLong来保存种子,并通过CAS(比较并交换)方式更新。这意味着在多线程同时调用同一个Random实例时,只有一个线程能成功更新种子,其余线程会自旋重试,从而引发竞争。
在单线程或者低并发环境中,这种开销可以忽略不计。基础用法非常简单,只需要创建实例并调用对应方法即可。下面的代码展示了如何用Random生成一个0到99之间的整数,以及一段连续的随机布尔值:
import java.util.Random;
public class RandomDemo {
public static void main(String[] args) {
// 使用系统时间作为种子创建Random
Random random = new Random();
// 生成[0, 100)之间的整数
int value = random.nextInt(100);
System.out.println("随机整数: " + value);
// 生成随机布尔值
boolean flag = random.nextBoolean();
System.out.println("随机布尔: " + flag);
// 生成随机浮点数,范围[0.0, 1.0)
double d = random.nextDouble();
System.out.println("随机浮点: " + d);
}
}
值得注意的是,如果你在循环中频繁创建Random实例,比如for (int i=0;i<1000;i++) new Random().nextInt(),由于每次构造都要关联系统时钟,不仅浪费内存,还可能在极短时间内产生相同种子,导致随机数序列重复。因此推荐将Random实例复用,或者在明确需要可复现结果时用固定种子,例如new Random(12345)。
Random还有一个容易被忽视的问题:它并不是加密安全的。如果业务涉及令牌、密钥、验证码等安全敏感场景,应该使用java.security.SecureRandom,而不是Random。Random的算法是公开的线性同余,知道部分输出就能推算后续序列,绝不能用于安全用途。
ThreadLocalRandom如何解决并发争用
从JDK 7开始,ThreadLocalRandom被引入到java.util.concurrent包中。它的核心思路是为每个线程分配独立的随机数生成状态,这些状态并不保存在共享对象里,而是直接存放在线程自身的字段中(通过Unsafe机制注入)。这样一来,线程在生成随机数时完全不需要访问共享变量,也就不存在锁或CAS竞争。
使用ThreadLocalRandom时,不能像Random那样直接new对象,而是要通过静态方法ThreadLocalRandom.current()获取当前线程对应的实例。之后调用方式与Random类似,但性能在多线程下有明显优势。下面是一段多线程测试示例,展示每个线程独立获取随机数的写法:
import java.util.concurrent.ThreadLocalRandom;
public class ThreadLocalRandomDemo {
public static void main(String[] args) {
Runnable task = () -> {
// 获取当前线程的ThreadLocalRandom实例
ThreadLocalRandom random = ThreadLocalRandom.current();
int num = random.nextInt(1, 101); // 生成[1, 101)即1到100
System.out.println(Thread.currentThread().getName() + " 得到: " + num);
};
// 启动多个线程验证无共享争用
for (int i = 0; i < 5; i++) {
new Thread(task).start();
}
}
}
ThreadLocalRandom不仅避免了竞争,还针对常见范围做了优化,例如nextInt(int origin, int bound)可以直接指定区间,而不需要像Random那样自己做取模运算。不过要注意,ThreadLocalRandom的实例不要跨线程传递使用,因为它绑定的是“调用current()那一刻”的线程,若把某个线程拿到的引用交给另一个线程用,会失去隔离性甚至拿到错误数据。
另外,ThreadLocalRandom同样不是加密安全的,它只是解决了性能与线程安全问题,并不提供不可预测性保障。如果在Web服务器中为每个请求生成追踪ID,用它可以大幅降低延迟;但若生成密码重置令牌,仍需SecureRandom。
性能对比与选型建议
我们可以通过简单的基准思路理解两者差异:假设有16个线程,每个线程循环一千万次调用nextInt()。使用共享Random时,由于AtomicLong的CAS失败重试,总耗时可能是ThreadLocalRandom的数倍甚至十倍以上;而ThreadLocalRandom因为零共享,基本随线程数线性扩展。虽然在单线程下两者差距很小,但现代Java服务几乎必然运行在线程池之上,因此并发环境默认选ThreadLocalRandom是更稳妥的做法。
选型上可以遵循以下原则:如果是小型单机工具、单元测试、或者明确单线程逻辑,用Random并复用实例即可;如果是Web应用、异步任务、并行流(parallel stream)里需要随机数,请统一用ThreadLocalRandom.current()。切忌将Random实例作为静态成员变量在多线程中共享,也切忌在并行流里用外部Random,因为并行流底层就是多线程拆分。下面的代码展示了并行流中的正确写法:
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class ParallelRandomDemo {
public static void main(String[] args) {
// 并行流中每个线程自行获取ThreadLocalRandom
List<Integer> list = IntStream.range(0, 100)
.parallel()
.mapToObj(i -> ThreadLocalRandom.current().nextInt(1000))
.collect(Collectors.toList());
System.out.println("生成数量: " + list.size());
}
}
最后提醒一点,无论用哪个类,随机数都只是“伪随机”,不能当作唯一ID的唯一来源。如果业务要求严格不重复,应该结合数据库自增、雪花算法或UUID,随机数最多只作为混淆因子。理清Random与ThreadLocalRandom的职责边界,才能既写出高性能代码,又避开并发陷阱。
RandomThreadLocalRandomJava随机数修改时间:2026-08-14 11:51:31