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

简介: 硬核 - Java 随机数相关 API 的演进与思考(下)
本系列将 Java 17 之前的随机数 API 以及 Java 17 之后的统一 API 都做了比较详细的说明,并且将随机数的特性以及实现思路也做了一些简单的分析,帮助大家明白为何会有这么多的随机数算法,以及他们的设计思路是什么。本系列会分为两篇,第一篇讲述 Java 随机数算法的演变思路以及底层原理与考量,之后介绍 Java 17 之前的随机算法 API 以及测试性能,第二篇详细分析 Java 17 之后的随机数生成器算法以及 API 和底层实现类以及他们的属性,性能以及使用场景,如何选择随机算法等等,并对 Java 的随机数对于 Java 的一些未来特性的适用进行展望

这是第二篇


Java 17 之后的变化


之前的 API 的缺点

  1. 没有统一的接口:之前的 Random 还有 SplittableRandom 没有统一的继承类,以及统一的抽象接口,虽然 他们内部方法基本一致,互相替换的麻烦并不多,但是这样我们要想实现自己的随机算法也比较麻烦,因为没有统一的接口。
  2. ThreadLocalRandom 与未来的 Project Loom 的虚拟线程相性比较差。虚拟线程是可以不断创建的资源,在大量虚拟线程中如果还是用 ThreadLocalRandom 一一对应的话,会有随机性减弱的问题。所以,我们需要寻找新的实现方法,并且从现在开始为 Project Loom 铺路。


新的 API 定义


在 Java 17 中的 JEP 356: Enhanced Pseudo-Random Number Generators 中,统一了随机数生成器的接口,即 RandomGenerator


image.png


其中,针对我们前面提到的可跳跃性(可以通过简单计算,跳过序列环中的很多元素)抽象了对应的接口 JumpableGenerator,如果跳跃的步长希望更大一些的话,对应的就是 LeapableGenerator

针对我们前面提到的可拆分性(可以通过简单计算,拆分出生成完全不同序列的随机数生成器)也抽象了接口 SplitableGenerator

前面提到的算法,与对应的实现类是:


image.png


统一抽象后,我们就可以这样创建不同实现类型的随机数字生成器:

RandomGenerator random = RandomGeneratorFactory.of("Random").create();
RandomGenerator secureRandom = RandomGeneratorFactory.of("SecureRandom").create();
RandomGenerator splittableRandom = RandomGeneratorFactory.of("SplittableRandom").create();
RandomGenerator xoroshiro128PlusPlus = RandomGeneratorFactory.of("Xoroshiro128PlusPlus").create();
RandomGenerator xoshiro256PlusPlus = RandomGeneratorFactory.of("Xoshiro256PlusPlus").create();
RandomGenerator l64X256MixRandom = RandomGeneratorFactory.of("L64X256MixRandom").create();
RandomGenerator l64X128StarStarRandom = RandomGeneratorFactory.of("L64X128StarStarRandom").create();
RandomGenerator l64X128MixRandom = RandomGeneratorFactory.of("L64X128MixRandom").create();
RandomGenerator l64X1024MixRandom = RandomGeneratorFactory.of("L64X1024MixRandom").create();
RandomGenerator l32X64MixRandom = RandomGeneratorFactory.of("L32X64MixRandom").create();
RandomGenerator l128X256MixRandom = RandomGeneratorFactory.of("L128X256MixRandom").create();
RandomGenerator l128X128MixRandom = RandomGeneratorFactory.of("L128X128MixRandom").create();
RandomGenerator l128X1024MixRandom = RandomGeneratorFactory.of("L128X1024MixRandom").create();


每种算法实现的随机数生成器类的属性


1.Random:底层是基于线性同余算法生成的是一个 48 位的数字,选择的参数保证了每个数字都能随机出来,所以 Period 为 2^48。nextInt 和 nextLong 都不能做到完全均匀随机分布,因为生成的数字是 48 位的数字,nextInt 即取其中的 32 位,nextLong 是取两次组合在一起。之前的算法分析我们提到过,这种算法不能跳跃,不能分割

2.SplittableRandom: 底层基于 SplitMix 算法生成的一个 64 位的数字,通过 MurMurhash 保证了区间内每个数字都会出现(所以 Period 是 2^64),并且是完全均匀分布的。对于 nextInt 是一个 Period 内每个结果都会出现两次,对于 nextLong 是一个 Period 内每个结果都会出现一次。之前的算法分析我们提到过,这种算法不能跳跃,可以分割

3.Xoroshiro128PlusPlus:底层基于 Xoroshiro128++ 算法,使用两个 64 位数字记录状态,这两个数字不会同时为 0,这两个数字经过计算组合成为一个 64 位随机数。由于是两个 64 位数字组合而成,那么就有 2^64 * 2^64 = 2^128 种不同组合,两个数字不会同时为 0,那么就少了一种情况,所以一共是 2^128 - 1 种情况,所以 Period 是 2^128 - 1。之前的算法分析我们提到过,这种算法可以跳跃,不能分割

4.Xoshiro256PlusPlus:底层基于 Xoshiro256++ 算法,使用四个 64 位数字记录状态,这四个数字不会同时为 0,这四个数字经过计算组合成为一个 64 位随机数。由于是四个 64 位数字组合而成,那么就有 2^64 * 2^64 * 2^64 * 2^64 = 2^256 种不同组合,两个数字不会同时为 0,那么就少了一种情况,所以一共是 2^256 - 1 种情况,所以 Period 是 2^256 - 1。之前的算法分析我们提到过,这种算法可以跳跃,不能分割

5.L64X256MixRandom:底层基于 LXM 算法,使用一个 64 位数字保存线性同余的结果,4 个 64 位数字记录 Xoshiro 组合,线性同余有 2^64种不同组合,Xoshiro2^256 - 1 种组合,一共 2^64(2^256 - 1) 种组合,所以 Period 是 2^64(2^256 - 1)。之前的算法分析我们提到过,这种算法可以分割,不能跳跃

其他的 LXM 实现类是类似的。


image.png


其实可以从每个算法的实现类的 `` 注解上,看出他们的这些属性:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface RandomGeneratorProperties {
    /**
     * 算法名称
     */
    String name();
    /**
     * 算法类别
     */
    String group() default "Legacy";
    /**
     * period 大小,由 i, j, k 三个数字描述,即:
     * period = (2^i - j) * 2^k
     */
    int i() default 0;
    int j() default 0;
    int k() default 0;
    /**
     * 均匀分布性,0 或者最大值则不是均匀分布,这个值描述在一个 Period 内每个数字出现多少次,但是不是那么精准的,会忽略一些小的偏差,例如 Xoroshiro128++ 认为每个数字出现 2^64 次而不是 2^64 - 1 次。
     */
    int equidistribution() default Integer.MAX_VALUE;
    /**
     * 是否是基于系统 Entropy(参考前面的 SEED 来源章节)的算法
     */
    boolean isStochastic() default false;
    /**
     * 是否是硬件辅助的算法
     */
    boolean isHardware() default false;
}

我们还可以通过下面的代码,查看每种实现的属性,同样的,也可以通过这些 API 对算法进行过滤,找到适合我们业务的实现类:

RandomGeneratorFactory.all()
  .map(fac -> fac.group()+":"+fac.name()
      + " {"
      + (fac.isSplittable()?" splitable":"")
      + (fac.isStreamable()?" streamable":"")
      + (fac.isJumpable()?" jumpable":"")
      + (fac.isLeapable()?" leapable":"")
      + (fac.isHardware()?" hardware":"")
      + (fac.isStatistical()?" statistical":"")
      + (fac.isStochastic()?" stochastic":"")
      + " stateBits: "+fac.stateBits()
      + " }"
  )
  .sorted().forEach(System.out::println);

输出是:

LXM:L128X1024MixRandom { splitable streamable statistical stateBits: 1152 }
LXM:L128X128MixRandom { splitable streamable statistical stateBits: 256 }
LXM:L128X256MixRandom { splitable streamable statistical stateBits: 384 }
LXM:L32X64MixRandom { splitable streamable statistical stateBits: 96 }
LXM:L64X1024MixRandom { splitable streamable statistical stateBits: 1088 }
LXM:L64X128MixRandom { splitable streamable statistical stateBits: 192 }
LXM:L64X128StarStarRandom { splitable streamable statistical stateBits: 192 }
LXM:L64X256MixRandom { splitable streamable statistical stateBits: 320 }
Legacy:Random { statistical stateBits: 48 }
Legacy:SecureRandom { stochastic stateBits: 2147483647 }
Legacy:SplittableRandom { splitable streamable statistical stateBits: 64 }
Xoroshiro:Xoroshiro128PlusPlus { streamable jumpable leapable statistical stateBits: 128 }
Xoshiro:Xoshiro256PlusPlus { streamable jumpable leapable statistical stateBits: 256 }


哪种算法最快(不用测也很明显)


这个根据之前的分析,应该还是 SplittableRandom 在单线程环境下最快,多线程环境下使用 ThreadLocalRandom 最快。新增的随机算法实现类,Period 约大需要的计算越多, LXM 的实现需要更多计算,加入这些算法是为了适应更多的随机应用,而不是为了更快。不过为了满足大家的好奇心,还是写了如下的代码进行测试,从下面的代码也可以看出,新的 RandomGenerator API 使用更加简便:

package prng;
import java.util.random.RandomGenerator;
import java.util.random.RandomGeneratorFactory;
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.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
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 TestRandomGenerator {
  @Param({
      "Random", "SecureRandom", "SplittableRandom", "Xoroshiro128PlusPlus", "Xoshiro256PlusPlus", "L64X256MixRandom",
      "L64X128StarStarRandom", "L64X128MixRandom", "L64X1024MixRandom", "L32X64MixRandom", "L128X256MixRandom",
      "L128X128MixRandom", "L128X1024MixRandom"
  })
  private String name;
  ThreadLocal<RandomGenerator> randomGenerator;
  @Setup
  public void setup() {
    final String finalName = this.name;
    randomGenerator = ThreadLocal.withInitial(() -> RandomGeneratorFactory.of(finalName).create());
  }
  @Benchmark
  public void testRandomInt(Blackhole blackhole) throws Exception {
    blackhole.consume(randomGenerator.get().nextInt());
  }
  @Benchmark
  public void testRandomIntWithBound(Blackhole blackhole) throws Exception {
    //注意不取 2^n 这种数字,因为这种数字一般不会作为实际应用的范围,但是底层针对这种数字有优化
    blackhole.consume(randomGenerator.get().nextInt(1, 100));
  }
  public static void main(String[] args) throws RunnerException {
    Options opt = new OptionsBuilder().include(TestRandomGenerator.class.getSimpleName()).build();
    new Runner(opt).run();
  }
}

测试结果:

Benchmark                                                  (name)   Mode  Cnt          Score           Error  Units
TestRandomGenerator.testRandomInt                          Random  thrpt   50  276250026.985 ± 240164319.588  ops/s
TestRandomGenerator.testRandomInt                    SecureRandom  thrpt   50    2362066.269 ±   1277699.965  ops/s
TestRandomGenerator.testRandomInt                SplittableRandom  thrpt   50  365417656.247 ± 377568150.497  ops/s
TestRandomGenerator.testRandomInt            Xoroshiro128PlusPlus  thrpt   50  341640250.941 ± 287261684.079  ops/s
TestRandomGenerator.testRandomInt              Xoshiro256PlusPlus  thrpt   50  343279172.542 ± 247888916.092  ops/s
TestRandomGenerator.testRandomInt                L64X256MixRandom  thrpt   50  317749688.838 ± 245196331.079  ops/s
TestRandomGenerator.testRandomInt           L64X128StarStarRandom  thrpt   50  294727346.284 ± 283056025.396  ops/s
TestRandomGenerator.testRandomInt                L64X128MixRandom  thrpt   50  314790625.909 ± 257860657.824  ops/s
TestRandomGenerator.testRandomInt               L64X1024MixRandom  thrpt   50  315040504.948 ± 101354716.147  ops/s
TestRandomGenerator.testRandomInt                 L32X64MixRandom  thrpt   50  311507435.009 ± 315893651.601  ops/s
TestRandomGenerator.testRandomInt               L128X256MixRandom  thrpt   50  187922591.311 ± 137220695.866  ops/s
TestRandomGenerator.testRandomInt               L128X128MixRandom  thrpt   50  218433110.870 ± 164229361.010  ops/s
TestRandomGenerator.testRandomInt              L128X1024MixRandom  thrpt   50  220855813.894 ±  47531327.692  ops/s
TestRandomGenerator.testRandomIntWithBound                 Random  thrpt   50  248088572.243 ± 206899706.862  ops/s
TestRandomGenerator.testRandomIntWithBound           SecureRandom  thrpt   50    1926592.946 ±   2060477.065  ops/s
TestRandomGenerator.testRandomIntWithBound       SplittableRandom  thrpt   50  334863388.450 ±  92778213.010  ops/s
TestRandomGenerator.testRandomIntWithBound   Xoroshiro128PlusPlus  thrpt   50  252787781.866 ± 200544008.824  ops/s
TestRandomGenerator.testRandomIntWithBound     Xoshiro256PlusPlus  thrpt   50  247673155.126 ± 164068511.968  ops/s
TestRandomGenerator.testRandomIntWithBound       L64X256MixRandom  thrpt   50  273735605.410 ±  87195037.181  ops/s
TestRandomGenerator.testRandomIntWithBound  L64X128StarStarRandom  thrpt   50  291151383.164 ± 192343348.429  ops/s
TestRandomGenerator.testRandomIntWithBound       L64X128MixRandom  thrpt   50  217051928.549 ± 177462405.951  ops/s
TestRandomGenerator.testRandomIntWithBound      L64X1024MixRandom  thrpt   50  222495366.798 ± 180718625.063  ops/s
TestRandomGenerator.testRandomIntWithBound        L32X64MixRandom  thrpt   50  305716905.710 ±  51030948.739  ops/s
TestRandomGenerator.testRandomIntWithBound      L128X256MixRandom  thrpt   50  174719656.589 ± 148285151.049  ops/s
TestRandomGenerator.testRandomIntWithBound      L128X128MixRandom  thrpt   50  176431895.622 ± 143002504.266  ops/s
TestRandomGenerator.testRandomIntWithBound     L128X1024MixRandom  thrpt   50  198282642.786 ±  24204852.619  ops/s

在之前的结果验证中,我们已经知道了 SplittableRandom 的在单线程中的性能最好,多线程环境下表现最好的是算法与它类似但是做了多线程优化的 ThreadLocalRandom.


如何选择随机算法


原则是,看你的业务场景,所有的随机组合到底有多少个,在什么范围内。然后找大于这个范围的 Period 中,性能最好的算法。例如,业务场景是一副扑克除了大小王 52 张牌,通过随机数决定发牌顺序:

  • 第一张牌:randomGenerator.nextInt(0, 52),从剩余的 52 张牌选
  • 第二张牌:randomGenerator.nextInt(0, 51),从剩余的 51 张牌选
  • 以此类推

那么一共有 52! 这么多结果,范围在 2^225 ~ 2^226 之间。如果我们使用的随机数生成器的 Period 小于这个结果集,那么某些牌的顺序,我们可能永远生成不了。所以,我们需要选择一个 Period > 54! 的随机数生成器。


未来展望


Project Loom 中的随机数生成器

如果 Project Loom 中没有针对 ThreadLocal 的优化,那么 ThreadLocalRandom 的随机性表现也会变差,因为虚拟线程是一个可以不断生成回收的类似于对象的东西,ThreadLocalRandom 并不能无限枚举下去。这时候我们可能需要使用固定的多个随机数生成器给所有的虚拟线程使用,而不是使用 ThreadLocalRandom:

ExecutorService vte = Executors.newVirtualThreadExecutor();
SplittableGenerator source = RandomGeneratorFactory.<SplittableGenerator>of("L128X1024MixRandom").create();
//分割出 128 个不同的随机数生成器
List<RandomGenerator> rngs = source.splits(128);
AtomicInteger i = new AtomicInteger(0);
vte.submit(() -> {
    long random = rngs.get(Math.abs(i.incrementAndGet() & 127)).nextLong();
    ...
});


Scoped Local 特性下的随机数生成器

Scoped Local 是一种更通用的域变量(例如 ThreadLocal 即当前线程域本地,Scoped Local 可以支持不同的域,包括虚拟线程,线程,方法域,包域等等),目前还没公布哪个版本会计划开发,不过按现在的爆料来看,我们可以使用下面这种方式更好的给每个线程分配随机数生成器:

private final static ScopeLocal<SplittableGenerator> rng_scope =
                                        ScopeLocal.inheritableForType(SplittableGenerator.class);
public static void main(String[] args) throws InterruptedException {
    SplittableGenerator rng1 =
            RandomGeneratorFactory.<SplittableGenerator>of("L128X1024MixRandom").create();
    SplittableGenerator rng2 =
            RandomGeneratorFactory.<SplittableGenerator>of("L32X64MixRandom").create();
    try (ExecutorService vte = Executors.newVirtualThreadExecutor()) {
        for (int i = 0; i < 5; i++) {
            ScopeLocal.where(rng_scope, rng1.split(), () -> { vte.submit(new Task()); });
        }
        for (int i = 0; i < 5; i++) {
            ScopeLocal.where(rng_scope, rng2.split(), () -> { vte.submit(new Task()); });
        }
    }
}
private static class Task implements Runnable {
    @Override public void run() {
        SplittableGenerator rng = rng_scope.get();
        System.out.println(rng);
    }
}



相关文章
|
2月前
|
Java API Maven
如何使用Java开发抖音API接口?
在数字化时代,社交媒体平台如抖音成为生活的重要部分。本文详细介绍了如何用Java开发抖音API接口,从创建开发者账号、申请API权限、准备开发环境,到编写代码、测试运行及注意事项,全面覆盖了整个开发流程。
266 10
|
2月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
96 2
|
21小时前
|
JSON Java 数据挖掘
利用 Java 代码获取淘宝关键字 API 接口
在数字化商业时代,精准把握市场动态与消费者需求是企业成功的关键。淘宝作为中国最大的电商平台之一,其海量数据中蕴含丰富的商业洞察。本文介绍如何通过Java代码高效、合规地获取淘宝关键字API接口数据,帮助商家优化产品布局、制定营销策略。主要内容包括: 1. **淘宝关键字API的价值**:洞察用户需求、优化产品标题与详情、制定营销策略。 2. **获取API接口的步骤**:注册账号、申请权限、搭建Java开发环境、编写调用代码、解析响应数据。 3. **注意事项**:遵守法律法规与平台规则,处理API调用限制。 通过这些步骤,商家可以在激烈的市场竞争中脱颖而出。
|
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代码实现和注意事项。通过遵循这些步骤,开发者可以高效地获取商品详情、描述及图片等信息,为项目和业务增添价值。
69 10
|
1月前
|
存储 Java 数据挖掘
Java 8 新特性之 Stream API:函数式编程风格的数据处理范式
Java 8 引入的 Stream API 提供了一种新的数据处理方式,支持函数式编程风格,能够高效、简洁地处理集合数据,实现过滤、映射、聚合等操作。
67 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为何值得选择。
77 2
|
2月前
|
缓存 监控 Java
如何运用JAVA开发API接口?
本文详细介绍了如何使用Java开发API接口,涵盖创建、实现、测试和部署接口的关键步骤。同时,讨论了接口的安全性设计和设计原则,帮助开发者构建高效、安全、易于维护的API接口。
218 4