Java 生成随机数
1.Random
Random 从 java1.0 开始就已经引入,是线程安全的。
初始化
Random 初始化时,默认采用 seeduniquifier 方法生成的 seed 和获取到的当前原子时钟的当前时间的与操作后的值来初始化一个随机数种子。因为 System.nanoTime() 是一直变化的,所以种子一定是每次都不一样的,默认初始化的源码如下:
public Random() {
this(seedUniquifier() ^ System.nanoTime());
}
/**
* seedUniquifier 解释
* 该方法是一个类似 while(true) 的无限循环 for (;;)
* 循环结束的条件是把 seedUniquifier 和一个常量值的乘积赋值给 seedUniquifier,然后判断是否等于 seedUniquifier.get()
*/
private static long seedUniquifier() {
// L'Ecuyer, "Tables of Linear Congruential Generators of
// Different Sizes and Good Lattice Structure", 1999
for (;;) {
long current = seedUniquifier.get();
long next = current * 1181783497276652981L;
if (seedUniquifier.compareAndSet(current, next))
return next;
}
}
private static final AtomicLong seedUniquifier
= new AtomicLong(8682522807148012L);
}
随机方法
其核心方法是 next 方法,不管是调用了 nextDouble 还是 nextInt 还是 nextBoolean,底层都是调这个 next(int bits):
/**
* 为了保证多线程下每次生成随机数都是用的不同,next() 得保证 seed 的更新是原子操作,所以用了 AtomicLong 的 compareAndSet(),以保证原子更新一个数。
* 当然也可以看出多个线程如果更新设置失败,会不停的在 while 循环执行,并且由于采用了多个线程共享一个 Random 实例。这样就会导致多个线程争用,出现性能上的问题
*/
protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
oldseed = seed.get();
nextseed = (oldseed * multiplier + addend) & mask;
} while (!seed.compareAndSet(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}
2.ThreadLocalRandom
为了在多线程并发情况下,减少多线程资源竞争,保证线程的安全性。java1.7 新增了 ThreadLocalRandom,继承于 Random。
初始化
因为构造器是默认访问权限,只能在 java.util 包中创建对象,故提供了一个方法 ThreadLocalRandom.current() 用于返回当前类的对象, 可以看到每个线程都持有一个本地的种子变量,该种子变量只有在使用随机数时才会被初始化。在多线程下计算新种子时,是根据自己线程内维护的种子变量进行更新,这就完全杜绝了线程间的竞争问题:
public static ThreadLocalRandom current() {
if (U.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
static final void localInit() {
int p = probeGenerator.addAndGet(PROBE_INCREMENT);
int probe = (p == 0) ? 1 : p; // skip 0
long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
Thread t = Thread.currentThread();
U.putLong(t, SEED, seed);
U.putInt(t, PROBE, probe);
}
随机方法
ThreadLocalRandom 是通过 ThreadLocal 改进的用于随机数生成的工具类,每个线程单独持有一个 ThreadLocalRandom 对象引用,这就完全杜绝了线程间的竞争问题:
final long nextSeed() {
Thread t; long r; // read and update per-thread seed
U.putLong(t = Thread.currentThread(), SEED,
r = U.getLong(t, SEED) + GAMMA);
return r;
}
使用 JMH 进行测试
随机 0-10
@BenchmarkMode(Mode.AverageTime) //平均时间
@State(Scope.Benchmark) //所有测试线程共享一个实例
@OutputTimeUnit(TimeUnit.NANOSECONDS) //统计结果的时间单位
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) //预热迭代 3 次,每次 1s
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) //实际测试 5 次,每次 1s
@Fork(2) //进行 fork 的次数
//@Threads(4)
public class RandomTest {
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(RandomTest.class.getSimpleName())
.result("random-result.json")
.resultFormat(ResultFormatType.JSON).build();
new Runner(opt).run();
}
@Benchmark
public void testRandom(Blackhole blackhole) {
Random random = new Random();
int result = random.nextInt(10);
System.out.println("random 随机数: " + result);
//JVM 可能会认为变量 result 从来没有使用过,从而进行优化把整个方法内部代码移除掉,这就会影响测试结果。
// JMH 提供了两种方式避免这种问题,一种是将这个变量作为方法返回值 return a,一种是通过 Blackhole 的 consume 来避免 JIT 的优化消除
blackhole.consume(result);
}
@Benchmark
public void testThreadLocalRandom(Blackhole blackhole) {
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
int result = threadLocalRandom.nextInt(10);
blackhole.consume(result);
}
}
测试结果
可以看到 ThreadLocalRandom 效率最高
SecureRandom
使用 Random 创建的是伪随机数,只要给定一个初始的种子,产生的随机数序列是完全一样的:
Random r = new Random(12345);
for (int i = 0; i < 10; i++) {
System.out.println(r.nextInt(100));
}
// 51, 80, 41, 28, 55...
}
不指定种子的话,默认的统当前时间戳作为种子,因此每次运行时,种子不同,得到的伪随机数序列就不同
Math.random() 实际上内部调用了 Random 类,所以它也是伪随机数,只是我们无法指定种子
而在有些场景中我们需要安全的随机数,即不能被预测到的,这个时候就需要用 SecureRandom
SecureRandom 的安全性是通过操作系统提供的安全的随机种子来生成随机数。这个种子是通过 CPU 的热噪声、读写磁盘的字节、网络流量等各种随机事件产生的“熵”。
SecureRandom sr = new SecureRandom();
System.out.println(sr.nextInt(100));
SecureRandom sr = null;
try {
sr = SecureRandom.getInstanceStrong(); // 获取高强度安全随机数生成器
} catch (NoSuchAlgorithmException e) {
sr = new SecureRandom(); // 获取普通的安全随机数生成器
}
byte[] buffer = new byte[16];
sr.nextBytes(buffer); // 用安全随机数填充 buffer
System.out.println(Arrays.toString(buffer));
更新
JDK 中生成随机数的类 java.util.Random,但是这个类生成的都是伪随机数。
JDK17 对这个类进行了加强,提供了一个 RandomGenerator 接口,为所有的伪随机数提供统一的 API。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论