Java随机数探秘

简介: 1 前言 一提到 Java 中的随机数,很多人就会想到 Ramdom,当出现生成随机数这样需求时,大多数人都会选择使用 Random 来生成随机数。Random 类是线程安全的,但其内部使用 CAS 来保证线程安全性,在多线程并发的情况下的时候它的表现是存在优化空间的。

1 前言

一提到 Java 中的随机数,很多人就会想到 Ramdom,当出现生成随机数这样需求时,大多数人都会选择使用 Random 来生成随机数。Random 类是线程安全的,但其内部使用 CAS 来保证线程安全性,在多线程并发的情况下的时候它的表现是存在优化空间的。在 JDK1.7 之后,Java 提供了更好的解决方案 ThreadLocalRandom,接下来,我们一起探讨下这几个随机数生成器的实现到底有何不同。

2 Random

Random 这个类是 JDK 提供的用来生成随机数的一个类,这个类并不是真正的随机,而是伪随机,伪随机的意思是生成的随机数其实是有一定规律的,而这个规律出现的周期随着伪随机算法的优劣而不同,一般来说周期比较长,但是可以预测。通过下面的代码我们可以对 Random 进行简单的使用:

fa2d9e5459c0a732454ca38d615f2ba99d8fafdf
Random原理

Random 中的方法比较多,这里就针对比较常见的 nextInt() 和 nextInt(int bound) 方法进行分析,前者会计算出 int 范围内的随机数,后者如果我们传入 10,那么他会求出 [0,10) 之间的 int 类型的随机数,左闭右开。我们首先看一下 Random() 的构造方法:

7573e99156ecb735faaa271f91a43a931f7bf4af

可以发现在构造方法当中,根据当前时间的种子生成了一个 AtomicLong 类型的 seed,这也是我们后续的关键所在。

nextInt()

nextInt() 的代码如下所示:

a3570342dd4f6b4270a4300adf7b955cbec930ff

这个里面直接调用的是 next() 方法,传入的 32,代指的是 Int 类型的位数。

77cfce3deeebe0dd1e6281f646caac242a7d30f5

这里会根据 seed 当前的值,通过一定的规则(伪随机算法)算出下一个 seed,然后进行 CAS,如果 CAS 失败则继续循环上面的操作。最后根据我们需要的 bit 位数来进行返回。核心便是 CAS 算法。

nextInt(int bound)

nextInt(int bound) 的代码如下所示:

9758626cc5e6e0eb3549045e8120c17bedbf078d

这个流程比 nextInt() 多了几步,具体步骤如下:

首先获取 31 位的随机数,注意这里是 31 位,和上面 32 位不同,因为在 nextInt() 方法中可以获取到随机数可能是负数,而 nextInt(int bound) 规定只能获取到 [0,bound) 之前的随机数,也就意味着必须是正数,预留一位符号位,所以只获取了31位。(不要想着使用取绝对值这样操作,会导致性能下降)

然后进行取 bound 操作。

如果 bound 是2的幂次方,可以直接将第一步获取的值乘以 bound 然后右移31位,解释一下:如果 bound 是4,那么乘以4其实就是左移2位,其实就是变成了33位,再右移31位的话,就又会变成2位,最后,2位 int 的范围其实就是 [0,4) 了。

如果不是 2 的幂,通过模运算进行处理。

并发瓶颈

在我之前的文章中就有相关的介绍,一般而言,CAS 相比加锁有一定的优势,但并不一定意味着高效。一个立刻被想到的解决方案是每次使用 Random 时都去 new 一个新的线程私有化的 Random 对象,或者使用 ThreadLocal 来维护线程私有化对象,但除此之外还存在更高效的方案,下面便来介绍本文的主角 ThreadLocalRandom。

3 ThreadLocalRandom

在 JDK1.7 之后提供了新的类 ThreadLocalRandom 用来在并发场景下代替 Random。使用方法比较简单:

 
 
  1. ThreadLocalRandom.current().nextInt();

  2. ThreadLocalRandom.current().nextInt(10);

在 current 方法中有:

241d92b608d7d79989329d826228cc6ace032901
可以看见如果没有初始化会对其进行初始化,而这里我们的 seed 不再是一个全局变量,在我们的Thread中有三个变量:
c3f6f2d768be049794e55d7a573ad7a64c0715f5

threadLocalRandomSeed:ThreadLocalRandom 使用它来控制随机数种子。

threadLocalRandomProbe:ThreadLocalRandom 使用它来控制初始化。

threadLocalRandomSecondarySeed:二级种子。

可以看见所有的变量都加了 @sun.misc.Contended 这个注解,用来处理伪共享问题。

在 nextInt() 方法当中代码如下:

dc02686acbcf9157ae7a62c20417480c73142568

我们的关键代码如下:

 
 
  1. UNSAFE.putLong(t = Thread.currentThread(), SEED,r=UNSAFE.getLong(t, SEED) + GAMMA);

可以看见由于我们每个线程各自都维护了种子,这个时候并不需要 CAS,直接进行 put,在这里利用线程之间隔离,减少了并发冲突;相比较 ThreadLocal<Random>,ThreadLocalRandom 不仅仅减少了对象维护的成本,其内部实现也更轻量级。所以 ThreadLocalRandom 性能很高。

4 性能测试

除了文章中详细介绍的 Random,ThreadLocalRandom,我还将 netty4 实现的 ThreadLocalRandom,以及 ThreadLocal<Random> 作为参考对象,一起参与 JMH 测评。

 
 
  1. @BenchmarkMode({Mode.AverageTime})

  2. @OutputTimeUnit(TimeUnit.NANOSECONDS)

  3. @Warmup(iterations = 3, time = 5)

  4. @Measurement(iterations = 3, time = 5)

  5. @Threads(50)

  6. @Fork(1)

  7. @State(Scope.Benchmark)

  8. public class RandomBenchmark {


  9. Random random = new Random();


  10. ThreadLocal<Random> threadLocalRandomHolder = ThreadLocal.withInitial(Random::new);


  11. @Benchmark

  12. public int random() {

  13. return random.nextInt();

  14. }


  15. @Benchmark

  16. public int threadLocalRandom() {

  17. return ThreadLocalRandom.current().nextInt();

  18. }


  19. @Benchmark

  20. public int threadLocalRandomHolder() {

  21. return threadLocalRandomHolder.get().nextInt();

  22. }


  23. @Benchmark

  24. public int nettyThreadLocalRandom() {

  25. return io.netty.util.internal.ThreadLocalRandom.current().nextInt();

  26. }


  27. public static void main(String[] args) throws RunnerException {

  28. Options opt = new OptionsBuilder()

  29. .include(RandomBenchmark.class.getSimpleName())

  30. .build();


  31. new Runner(opt).run();

  32. }


  33. }

测评结果如下:

 
 
  1. Benchmark Mode Cnt Score Error Units

  2. RandomBenchmark.nettyThreadLocalRandom avgt 3 192.202 ± 295.897 ns/op

  3. RandomBenchmark.random avgt 3 3197.620 ± 380.981 ns/op

  4. RandomBenchmark.threadLocalRandom avgt 3 90.731 ± 39.098 ns/op

  5. RandomBenchmark.threadLocalRandomHolder avgt 3 229.502 ± 267.144 ns/op

从上图可以发现,JDK1.7 的 ThreadLocalRandom 取得了最好的成绩,仅仅需要 90 ns 就可以生成一次随机数,netty 实现的 ThreadLocalRandom 以及使用 ThreadLocal 维护 Random 的方式差距不是很大,位列 2、3 位,共享的 Random 变量则效果最差。

可见,在并发场景下,ThreadLocalRandom 可以明显的提升性能。
5 注意点

注意,ThreadLocalRandom 切记不要调用 current 方法之后,作为共享变量使用

 
 
  1. public class WrongCase {


  2. ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();


  3. public int concurrentNextInt(){

  4. return threadLocalRandom.nextInt();

  5. }


  6. }

这是因为 ThreadLocalRandom.current() 会使用初始化它的线程来填充随机种子,这会带来导致多个线程使用相同的 seed。

 
 
  1. public class Main {


  2. public static void main(String[] args) {

  3. ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();

  4. for(int i=0;i<10;i++)

  5. new Thread(new Runnable() {

  6. @Override

  7. public void run() {

  8. System.out.println(threadLocalRandom.nextInt());

  9. }

  10. }).start();


  11. }

  12. }

输出相同的随机数:

 
 
  1. -1667209487

  2. -1667209487

  3. -1667209487

  4. -1667209487

  5. -1667209487

  6. -1667209487

  7. -1667209487

  8. -1667209487

  9. -1667209487

  10. -1667209487

请在确保不同线程获取不同的 seed,最简单的方式便是每次调用都是使用 current():

 
 
  1. public class RightCase {

  2. public int concurrentNextInt(){

  3. return ThreadLocalRandom.current().nextInt();

  4. }

  5. }

彩蛋1

梁飞博客中一句话常常在我脑海中萦绕:魔鬼在细节中。优秀的代码都是一个个小细节堆砌出来,今天介绍的 ThreadLocalRandom 也不例外。

6bdff810ca689e3352d31f0b0311a6bffef035f4

在 incubator-dubbo-2.7.0 中,随机负载均衡器的一个小改动便是将 Random 替换为了 ThreadLocalRandom,用于优化并发性能。

彩蛋2

ThreadLocalRandom 的 nextInt(int bound) 方法中,当 bound 不为 2 的幂次方时,进入 else 分支,使用了一个循环来修改 r 的值,我认为这可能不必要,你觉得呢?

 
 
  1. public int nextInt(int bound) {

  2. if (bound <= 0)

  3. throw new IllegalArgumentException(BadBound);

  4. int r = mix32(nextSeed());

  5. int m = bound - 1;

  6. if ((bound & m) == 0) // power of two

  7. r &= m;

  8. else { // reject over-represented candidates

  9. for (int u = r >>> 1;

  10. u + m - (r = u % bound) < 0;

  11. u = mix32(nextSeed()) >>> 1)

  12. ;

  13. }

  14. return r;

  15. }

原文发布时间为:2018-09-12
本文作者:Kirito的技术分享
本文来自云栖社区合作伙伴“
Kirito的技术分享”,了解相关信息可以关注“Kirito的技术分享”。

相关文章
|
3月前
|
Java
Java产生随机数
Java产生随机数
41 0
|
7月前
|
Java 数据安全/隐私保护 索引
使用Java中的随机函数生成随机数
在Java编程中,我们经常需要生成随机数来模拟各种情况或者作为密码等敏感信息的一部分。Java提供了一个内置的随机函数库,可以轻松地生成各种类型的随机数。本篇博客将介绍如何使用Java中的随机函数来生成随机数。
119 1
|
18天前
|
Java
java_键盘录入、随机数
本文介绍了Java中键盘录入和Random类的使用。键盘录入用于从用户那里获取数据,通过导入`java.util.Scanner`,创建`Scanner`对象,调用`nextInt()`或`nextDouble()`读取整数和小数,`next()`读取字符串。Random类用于生成随机整数,导入该类后创建对象,调用`nextInt(int bound)`生成[0, bound-1]范围内的随机数。在JDK17及以上版本,可以使用`nextInt(int start, int end)`生成[start, end)范围的随机数。常见应用包括猜数字游戏和随机点名。
13 0
|
1月前
|
存储 Java
35、Java 中的 Math 类、Random 随机数、UUID、格式化字符串或数字、字符串和数字的相互转换、高精度计算、BigDecimal、计算机中的浮点数都是近似值
35、Java 中的 Math 类、Random 随机数、UUID、格式化字符串或数字、字符串和数字的相互转换、高精度计算、BigDecimal、计算机中的浮点数都是近似值
48 0
|
4月前
|
Java API
Java的Math.random获取区间随机数
Java的Math.random获取区间随机数
|
4月前
|
Java
Java常用类中随机数讲解与实践
Java常用类中随机数讲解与实践
40 0
|
5月前
|
安全 Java 数据安全/隐私保护
java random随机数的用法
java random随机数的用法
|
7月前
|
Java
如何使用Java实现随机数生成器
在Java编程中,需要生成随机数的情况非常常见。本文将介绍如何使用Java中提供的相关类和方法来实现随机数生成器。
72 1
|
8月前
|
存储 安全 Java
Java类库StrringBuffer类、Math、SimpleDateFormat、Random随机数、大数字处理类和UUID 无重复数据(附带面试题)
1.StrringBuffer类 CharConsequnse接口,2.Math数学计算,3.Date日期处理类与SimpleDateFormat、4.Random随机数、5.大数字处理类、6.UUID 无重复数据
76 0