硬核 - Java 随机数相关 API 的演进与思考(上3)

简介: 硬核 - Java 随机数相关 API 的演进与思考(上3)
+关注继续查看

Java 17 之前一般如何生成随机数以及对应的随机算法


首先放出算法与实现类的对应关系:


image.png


使用 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());

但是由于没有做对齐性填充以及其他一些多线程性能优化的东西,导致其多线程环境下的性能表现还是比基于 SplittableRandomThreadLocalRandom要差。

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 的性能最好。单线程环境下 SplittableRandomThreadLocalRandom 基本接近,性能要好于其他的。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();
    }
}
相关文章
|
5天前
|
JSON Java 测试技术
Elasticsearch常用Java API编程 2
Elasticsearch常用Java API编程
14 0
|
5天前
|
JSON Java 测试技术
Elasticsearch常用Java API编程 1
Elasticsearch常用Java API编程
24 0
|
9天前
|
Java API
Java 中如何使用 Stream API?
Java 中如何使用 Stream API?
|
10天前
|
Java API 数据安全/隐私保护
Elasticsearch Java API Client 开发
本场景主要介绍如何使用 Elasticsearch Java API Client 进行开发,实现常用的 CRUD 操作。
36 0
|
14天前
|
存储 Java API
Java之API详解之BigDecimal类的详细解析
Java之API详解之BigDecimal类的详细解析
22 0
|
14天前
|
存储 Java API
Java之API详解之Biginteger类的详解
Java之API详解之Biginteger类的详解
30 0
|
14天前
|
Java API
Java之API详解之Objects类的详细解析
Java之API详解之Objects类的详细解析
13 0
|
14天前
|
Java API 开发工具
Java之API详解之Object类的详细解析(下)
Java之API详解之Object类的详细解析(下)
10 0
|
14天前
|
Java API 开发工具
Java之API详解之Object类的详细解析(上)
Java之API详解之Object类的详细解析
12 0
|
14天前
|
Java API
Java之API详解之Runtime的详细解析
Java之API详解之Runtime的详细解析
19 0
Java之API详解之Runtime的详细解析
相关产品
云迁移中心
推荐文章
更多