本系列将 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

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

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

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

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

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 实现类是类似的。

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

@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);
}
}

微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer

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

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

    本系列将 Java 17 之前的随机数 API 以及 Java 17 之后的统一 API 都做了比较详细的说明,并且将随机数的特性以及实现思路也做了一些简单的分析,帮助大家明白为何会有这么多的随机数算 ...

  2. 全网最硬核 Java 新内存模型解析与实验单篇版(不断更新QA中)

    个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判.如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 i ...

  3. 硬核测试:Pulsar 与 Kafka 在金融场景下的性能分析

    背景 Apache Pulsar 是下一代分布式消息流平台,采用计算存储分层架构,具备多租户.高一致.高性能.百万 topic.数据平滑迁移等诸多优势.越来越多的企业正在使用 Pulsar 或者尝试将 ...

  4. Mybatis系列全解(六):Mybatis最硬核的API你知道几个?

    封面:洛小汐 作者:潘潘 2020 年的大疫情,把世界撕成几片. 时至今日,依旧人心惶惶. 很庆幸,身处这安稳国, 兼得一份安稳工. · 东家常讲的一个词:深秋心态 . 大势时,不跟风.起哄, 萧条时 ...

  5. 阿里P7整理“硬核”面试文档:Java基础+数据库+算法+框架技术等

    现在的程序员越来越多,大部分的程序员都想着自己能够进入大厂工作,但每个人的能力都是有差距的,所以并不是人人都能跨进BATJ.即使如此,但身在职场的我们一刻也不能懈怠,既然对BATJ好奇,那么就要朝这个 ...

  6. [原创]java WEB学习笔记44:Filter 简介,模型,创建,工作原理,相关API,过滤器的部署及映射的方式,Demo

    本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当 ...

  7. 【Socket编程】Java中网络相关API的应用

    Java中网络相关API的应用 一.InetAddress类 InetAddress类用于标识网络上的硬件资源,表示互联网协议(IP)地址. InetAddress类没有构造方法,所以不能直接new出 ...

  8. java 11 移除的一些其他内容,更简化的编译运行程序,Unicode 10,移除了不太使用的JavaEE模块和CORBA技术,废除Nashorn javascript引擎,不建议使用Pack200 相关api

    移除的一些其他内容 移除项 移除了com.sun.awt.AWTUtilities 移除了sun.misc.Unsafe.defineClass, 使用java.lang.invoke.MethodH ...

  9. 文件IO 相关的包:java.io文件——API

    文件IO 相关的包:java.io文件——API 1.Java.io.File类的使用(1)两种路径绝对路径:相对于当前路径:当前为 “工程名”(2)File类创建,对象为一个文件/目录,可能存在或不 ...

随机推荐

  1. < 转>Java 反射机制浅析

    一.什么是反射: 反射的概念是由Smith在1982年首次提出的,主要是指程序可以访问.检测和修改它本身状态或行为的一种能力.这一概念的提出很快引发了计算机科学领 域关于应用反射性的研究.它首先被程序 ...

  2. 拆分函数Splitter.Split…(Power Query 之 M 语言)

    按相同分隔符拆分: =Splitter.SplitTextByDelimiter("拆分符号", 引号字符) 拆分符号 直接输入 特殊符号 制表符:#(tab) 回车:#(cr) ...

  3. SpringCloud Alibaba实战(12:引入Dubbo实现RPC调用)

    源码地址:https://gitee.com/fighter3/eshop-project.git 持续更新中-- 大家好,我是老三,断更了半年,我又滚回来继续写这个系列了,还有人看吗-- 在前面的章 ...

  4. 5款超实用的.NET性能分析工具

    虽然.NET框架号称永远不会发生内存泄漏,原因是引入了内存回收机制.但在实际应用中,往往我们分配了对象但没有释放指向该对象的引用,导致对象永远无法释放.最常见的情况就是给对象添加了事件处理函数,但当不 ...

  5. 四、Uniapp+vue+腾讯IM+腾讯音视频开发仿微信的IM聊天APP,支持各类消息收发,音视频通话,附vue实现源码(已开源)-会话好友列表的实现

    会话好友列表的实现 1.项目引言 2.腾讯云后台配置TXIM 3.配置项目并实现IM登录 4.会话好友列表的实现 5.聊天输入框的实现 6.聊天界面容器的实现 7.聊天消息项的实现 8.聊天输入框扩展 ...

  6. 【LeetCode】160. Intersection of Two Linked Lists 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 双指针 栈 日期 题目地址:https://leet ...

  7. 【剑指Offer】反转链表 解题报告(Python)

    [剑指Offer]反转链表 解题报告(Python) 标签(空格分隔): LeetCode 题目地址:https://www.nowcoder.com/ta/coding-interviews 题目描 ...

  8. 【LeetCode】331. Verify Preorder Serialization of a Binary Tree 解题报告(Python)

    [LeetCode]331. Verify Preorder Serialization of a Binary Tree 解题报告(Python) 标签: LeetCode 题目地址:https:/ ...

  9. Andrey and Problem

    B. Andrey and Problem time limit per test 2 seconds memory limit per test 256 megabytes input standa ...

  10. codeforces626D . Jerry's Protest

    Andrew and Jerry are playing a game with Harry as the scorekeeper. The game consists of three rounds ...