Java 17 之前一般如何生成随机数以及对应的随机算法
首先放出算法与实现类的对应关系:
使用 JDK 的 API
1.使用java.util.Random
和基于它的 API:
Random random = new Random(); random.nextInt();
Math.random()
底层也是基于 Random
java.lang.Math
:
public static double random() { return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble(); } private static final class RandomNumberGeneratorHolder { static final Random randomNumberGenerator = new Random(); }
Random
本身是设计成线程安全的,因为 SEED 是 Atomic 的并且随机只是 CAS 更新这个 SEED:
java.util.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)); }
同时也看出,Random 是基于线性同余算法的
2.使用java.util.SplittableRandom
和基于它的 API
SplittableRandom splittableRandom = new SplittableRandom(); splittableRandom.nextInt();
前面的分析我们提到了,SplittableRandom 基于 SplitMix 算法实现,即给定一个初始 SEED,设置一个固定步长 M,每次随机,将这个 SEED 加上步长 M,经过一个 HASH 函数(这里是 MurMurHash3),将这个值散列映射到一个 HASH 值。
SplittableRandom
本身不是线程安全的: java.util.SplittableRandom
:
public int nextInt() { return mix32(nextSeed()); } private long nextSeed() { //这里非线程安全 return seed += gamma; }
ThreadLocalRandom
基于SplittableRandom
实现,我们在多线程环境下使用 ThreadLocalRandom
:
ThreadLocalRandom.current().nextInt();
SplittableRandom
可以通过 split 方法返回一个参数全新,随机序列特性差异很大的新的 SplittableRandom
,我们可以将他们用于不同的线程生成随机数,这在 parallel Stream 中非常常见:
IntStream.range(0, 1000) .parallel() .map(index -> usersService.getUsersByGood(index)) .map(users -> users.get(splittableRandom.split().nextInt(users.size()))) .collect(Collectors.toList());
但是由于没有做对齐性填充以及其他一些多线程性能优化的东西,导致其多线程环境下的性能表现还是比基于 SplittableRandom
的 ThreadLocalRandom
要差。
3. 使用java.security.SecureRandom
生成安全性更高的随机数
SecureRandom drbg = SecureRandom.getInstance("DRBG"); drbg.nextInt();
一般这种算法,基于加密算法实现,计算更加复杂,性能也比较差,只有安全性非常敏感的业务才会使用,一般业务(例如抽奖)这些是不会使用的。
测试性能
单线程测试:
Benchmark Mode Cnt Score Error Units TestRandom.testDRBGSecureRandomInt thrpt 50 940907.223 ± 11505.342 ops/s TestRandom.testDRBGSecureRandomIntWithBound thrpt 50 992789.814 ± 71312.127 ops/s TestRandom.testRandomInt thrpt 50 106491372.544 ± 8881505.674 ops/s TestRandom.testRandomIntWithBound thrpt 50 99009878.690 ± 9411874.862 ops/s TestRandom.testSplittableRandomInt thrpt 50 295631145.320 ± 82211818.950 ops/s TestRandom.testSplittableRandomIntWithBound thrpt 50 190550282.857 ± 17108994.427 ops/s TestRandom.testThreadLocalRandomInt thrpt 50 264264886.637 ± 67311258.237 ops/s TestRandom.testThreadLocalRandomIntWithBound thrpt 50 162884175.411 ± 12127863.560 ops/s
多线程测试:
Benchmark Mode Cnt Score Error Units TestRandom.testDRBGSecureRandomInt thrpt 50 2492896.096 ± 19410.632 ops/s TestRandom.testDRBGSecureRandomIntWithBound thrpt 50 2478206.361 ± 111106.563 ops/s TestRandom.testRandomInt thrpt 50 345345082.968 ± 21717020.450 ops/s TestRandom.testRandomIntWithBound thrpt 50 300777199.608 ± 17577234.117 ops/s TestRandom.testSplittableRandomInt thrpt 50 465579146.155 ± 25901118.711 ops/s TestRandom.testSplittableRandomIntWithBound thrpt 50 344833166.641 ± 30676425.124 ops/s TestRandom.testThreadLocalRandomInt thrpt 50 647483039.493 ± 120906932.951 ops/s TestRandom.testThreadLocalRandomIntWithBound thrpt 50 467680021.387 ± 82625535.510 ops/s
结果和我们之前说明的预期基本一致,多线程环境下 ThreadLocalRandom
的性能最好。单线程环境下 SplittableRandom
和 ThreadLocalRandom
基本接近,性能要好于其他的。SecureRandom
和其他的相比性能差了几百倍。
测试代码如下(注意虽然 Random 和 SecureRandom 都是线程安全的,但是为了避免 compareAndSet 带来的性能衰减过多,还是用了 ThreadLocal。):
package prng; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Random; import java.util.SplittableRandom; import java.util.concurrent.ThreadLocalRandom; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.Threads; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; //测试指标为吞吐量 @BenchmarkMode(Mode.Throughput) //需要预热,排除 jit 即时编译以及 JVM 采集各种指标带来的影响,由于我们单次循环很多次,所以预热一次就行 @Warmup(iterations = 1) //线程个数 @Threads(10) @Fork(1) //测试次数,我们测试50次 @Measurement(iterations = 50) //定义了一个类实例的生命周期,所有测试线程共享一个实例 @State(value = Scope.Benchmark) public class TestRandom { ThreadLocal<Random> random = ThreadLocal.withInitial(Random::new); ThreadLocal<SplittableRandom> splittableRandom = ThreadLocal.withInitial(SplittableRandom::new); ThreadLocal<SecureRandom> drbg = ThreadLocal.withInitial(() -> { try { return SecureRandom.getInstance("DRBG"); } catch (NoSuchAlgorithmException e) { throw new IllegalArgumentException(e); } }); @Benchmark public void testRandomInt(Blackhole blackhole) throws Exception { blackhole.consume(random.get().nextInt()); } @Benchmark public void testRandomIntWithBound(Blackhole blackhole) throws Exception { //注意不取 2^n 这种数字,因为这种数字一般不会作为实际应用的范围,但是底层针对这种数字有优化 blackhole.consume(random.get().nextInt(1, 100)); } @Benchmark public void testSplittableRandomInt(Blackhole blackhole) throws Exception { blackhole.consume(splittableRandom.get().nextInt()); } @Benchmark public void testSplittableRandomIntWithBound(Blackhole blackhole) throws Exception { //注意不取 2^n 这种数字,因为这种数字一般不会作为实际应用的范围,但是底层针对这种数字有优化 blackhole.consume(splittableRandom.get().nextInt(1, 100)); } @Benchmark public void testThreadLocalRandomInt(Blackhole blackhole) throws Exception { blackhole.consume(ThreadLocalRandom.current().nextInt()); } @Benchmark public void testThreadLocalRandomIntWithBound(Blackhole blackhole) throws Exception { //注意不取 2^n 这种数字,因为这种数字一般不会作为实际应用的范围,但是底层针对这种数字有优化 blackhole.consume(ThreadLocalRandom.current().nextInt(1, 100)); } @Benchmark public void testDRBGSecureRandomInt(Blackhole blackhole) { blackhole.consume(drbg.get().nextInt()); } @Benchmark public void testDRBGSecureRandomIntWithBound(Blackhole blackhole) { //注意不取 2^n 这种数字,因为这种数字一般不会作为实际应用的范围,但是底层针对这种数字有优化 blackhole.consume(drbg.get().nextInt(1, 100)); } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder().include(TestRandom.class.getSimpleName()).build(); new Runner(opt).run(); } }