硬核 - 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();
  }
}
相关文章
|
2月前
|
Java API Maven
如何使用Java开发抖音API接口?
在数字化时代,社交媒体平台如抖音成为生活的重要部分。本文详细介绍了如何用Java开发抖音API接口,从创建开发者账号、申请API权限、准备开发环境,到编写代码、测试运行及注意事项,全面覆盖了整个开发流程。
263 10
|
2月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
96 2
|
17天前
|
JSON Java Apache
Java基础-常用API-Object类
继承是面向对象编程的重要特性,允许从已有类派生新类。Java采用单继承机制,默认所有类继承自Object类。Object类提供了多个常用方法,如`clone()`用于复制对象,`equals()`判断对象是否相等,`hashCode()`计算哈希码,`toString()`返回对象的字符串表示,`wait()`、`notify()`和`notifyAll()`用于线程同步,`finalize()`在对象被垃圾回收时调用。掌握这些方法有助于更好地理解和使用Java中的对象行为。
|
1月前
|
算法 Java API
如何使用Java开发获得淘宝商品描述API接口?
本文详细介绍如何使用Java开发调用淘宝商品描述API接口,涵盖从注册淘宝开放平台账号、阅读平台规则、创建应用并申请接口权限,到安装开发工具、配置开发环境、获取访问令牌,以及具体的Java代码实现和注意事项。通过遵循这些步骤,开发者可以高效地获取商品详情、描述及图片等信息,为项目和业务增添价值。
65 10
|
1月前
|
存储 Java 数据挖掘
Java 8 新特性之 Stream API:函数式编程风格的数据处理范式
Java 8 引入的 Stream API 提供了一种新的数据处理方式,支持函数式编程风格,能够高效、简洁地处理集合数据,实现过滤、映射、聚合等操作。
65 6
|
1月前
|
Java API 开发者
Java中的Lambda表达式与Stream API的协同作用
在本文中,我们将探讨Java 8引入的Lambda表达式和Stream API如何改变我们处理集合和数组的方式。Lambda表达式提供了一种简洁的方法来表达代码块,而Stream API则允许我们对数据流进行高级操作,如过滤、映射和归约。通过结合使用这两种技术,我们可以以声明式的方式编写更简洁、更易于理解和维护的代码。本文将介绍Lambda表达式和Stream API的基本概念,并通过示例展示它们在实际项目中的应用。
|
2月前
|
安全 Java API
告别SimpleDateFormat:Java 8日期时间API的最佳实践
在Java开发中,处理日期和时间是一个基本而重要的任务。传统的`SimpleDateFormat`类因其简单易用而被广泛采用,但它存在一些潜在的问题,尤其是在多线程环境下。本文将探讨`SimpleDateFormat`的局限性,并介绍Java 8引入的新的日期时间API,以及如何使用这些新工具来避免潜在的风险。
42 5
|
2月前
|
开发框架 Java 关系型数据库
Java哪个框架适合开发API接口?
在快速发展的软件开发领域,API接口连接了不同的系统和服务。Java作为成熟的编程语言,其生态系统中出现了许多API开发框架。Magic-API因其独特优势和强大功能,成为Java开发者优选的API开发框架。本文将从核心优势、实际应用价值及未来展望等方面,深入探讨Magic-API为何值得选择。
76 2
|
2月前
|
缓存 监控 Java
如何运用JAVA开发API接口?
本文详细介绍了如何使用Java开发API接口,涵盖创建、实现、测试和部署接口的关键步骤。同时,讨论了接口的安全性设计和设计原则,帮助开发者构建高效、安全、易于维护的API接口。
214 4
|
2月前
|
安全 Java API
Java中的Lambda表达式与Stream API的高效结合####
探索Java编程中Lambda表达式与Stream API如何携手并进,提升数据处理效率,实现代码简洁性与功能性的双重飞跃。 ####
33 0
下一篇
开通oss服务