内容简介:本文的前3节参考修改自微信公众号「咖啡拿铁」的文章,感谢李钊同学对这个话题热情的讨论。一提到 Java 中的随机数,很多人就会想到Random 这个类是 JDK 提供的用来生成随机数的一个类,这个类并不是真正的随机,而是伪随机,伪随机的意思是生成的随机数其实是有一定规律的,而这个规律出现的周期随着伪随机算法的优劣而不同,一般来说周期比较长,但是可以预测。通过下面的代码我们可以对 Random 进行简单的使用:
本文的前3节参考修改自微信公众号「咖啡拿铁」的文章,感谢李钊同学对这个话题热情的讨论。
1 前言
一提到 Java 中的随机数,很多人就会想到 Ramdom
,当出现生成随机数这样需求时,大多数人都会选择使用 Random 来生成随机数。Random 类是线程安全的,但其内部使用 CAS 来保证线程安全性,在多线程并发的情况下的时候它的表现是存在优化空间的。在 JDK1.7 之后,Java 提供了更好的解决方案 ThreadLocalRandom,接下来,我们一起探讨下这几个随机数生成器的实现到底有何不同。
2 Random
Random 这个类是 JDK 提供的用来生成随机数的一个类,这个类并不是真正的随机,而是伪随机,伪随机的意思是生成的随机数其实是有一定规律的,而这个规律出现的周期随着伪随机算法的优劣而不同,一般来说周期比较长,但是可以预测。通过下面的代码我们可以对 Random 进行简单的使用:
Random原理
Random 中的方法比较多,这里就针对比较常见的 nextInt() 和 nextInt(int bound) 方法进行分析,前者会计算出 int 范围内的随机数,后者如果我们传入 10,那么他会求出 [0,10) 之间的 int 类型的随机数,左闭右开。我们首先看一下 Random() 的构造方法:
可以发现在构造方法当中,根据当前时间的种子生成了一个 AtomicLong 类型的 seed,这也是我们后续的关键所在。
####nextInt()
nextInt() 的代码如下所示:
这个里面直接调用的是 next() 方法,传入的 32,代指的是 Int 类型的位数。
这里会根据 seed 当前的值,通过一定的规则(伪随机算法)算出下一个 seed,然后进行 CAS,如果 CAS 失败则继续循环上面的操作。最后根据我们需要的 bit 位数来进行返回。核心便是 CAS 算法。
nextInt(int bound)
nextInt(int bound) 的代码如下所示:
这个流程比 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。使用方法比较简单:
ThreadLocalRandom.current().nextInt(); ThreadLocalRandom.current().nextInt(10);
在 current 方法中有:
可以看见如果没有初始化会对其进行初始化,而这里我们的 seed 不再是一个全局变量,在我们的Thread中有三个变量:
- threadLocalRandomSeed:ThreadLocalRandom 使用它来控制随机数种子。
- threadLocalRandomProbe:ThreadLocalRandom 使用它来控制初始化。
- threadLocalRandomSecondarySeed:二级种子。
可以看见所有的变量都加了 @sun.misc.Contended 这个注解,用来处理伪共享问题。
在 nextInt() 方法当中代码如下:
我们的关键代码如下:
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 测评。
@BenchmarkMode({Mode.AverageTime}) @OutputTimeUnit(TimeUnit.NANOSECONDS) @Warmup(iterations = 3, time = 5) @Measurement(iterations = 3, time = 5) @Threads(50) @Fork(1) @State(Scope.Benchmark) public class RandomBenchmark { Random random = new Random(); ThreadLocal<Random> threadLocalRandomHolder = ThreadLocal.withInitial(Random::new); @Benchmark public int random() { return random.nextInt(); } @Benchmark public int threadLocalRandom() { return ThreadLocalRandom.current().nextInt(); } @Benchmark public int threadLocalRandomHolder() { return threadLocalRandomHolder.get().nextInt(); } @Benchmark public int nettyThreadLocalRandom() { return io.netty.util.internal.ThreadLocalRandom.current().nextInt(); } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(RandomBenchmark.class.getSimpleName()) .build(); new Runner(opt).run(); } }
测评结果如下:
Benchmark Mode Cnt Score Error Units RandomBenchmark.nettyThreadLocalRandom avgt 3 192.202 ± 295.897 ns/op RandomBenchmark.random avgt 3 3197.620 ± 380.981 ns/op RandomBenchmark.threadLocalRandom avgt 3 90.731 ± 39.098 ns/op 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 方法之后,作为共享变量使用
public class WrongCase { ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current(); public int concurrentNextInt(){ return threadLocalRandom.nextInt(); } }
这是因为 ThreadLocalRandom.current() 会使用初始化它的线程来填充随机种子,这会带来导致多个线程使用相同的 seed。
public class Main { public static void main(String[] args) { ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current(); for(int i=0;i<10;i++) new Thread(new Runnable() { @Override public void run() { System.out.println(threadLocalRandom.nextInt()); } }).start(); } }
输出相同的随机数:
-1667209487 -1667209487 -1667209487 -1667209487 -1667209487 -1667209487 -1667209487 -1667209487 -1667209487 -1667209487
请在确保不同线程获取不同的 seed,最简单的方式便是每次调用都是使用 current():
public class RightCase { public int concurrentNextInt(){ return ThreadLocalRandom.current().nextInt(); } }
彩蛋1
梁飞博客中一句话常常在我脑海中萦绕:魔鬼在细节中。优秀的代码都是一个个小细节堆砌出来,今天介绍的 ThreadLocalRandom 也不例外。
在 incubator-dubbo-2.7.0 中,随机负载均衡器的一个小改动便是将 Random 替换为了 ThreadLocalRandom,用于优化并发性能。
彩蛋2
ThreadLocalRandom 的 nextInt(int bound) 方法中,当 bound 不为 2 的幂次方时,使用了一个循环来修改 r 的值,我认为这可能不必要,你觉得呢?
public int nextInt(int bound) { if (bound <= 0) throw new IllegalArgumentException(BadBound); int r = mix32(nextSeed()); int m = bound - 1; if ((bound & m) == 0) // power of two r &= m; else { // reject over-represented candidates for (int u = r >>> 1; u + m - (r = u % bound) < 0; u = mix32(nextSeed()) >>> 1) ; } return r; }
欢迎关注李钊同学的微信公众号:「咖啡拿铁」
当然,也欢迎关注我的微信公众号:「Kirito的技术分享」,关于文章的任何疑问都会得到回复,带来更多 Java 相关的技术分享。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Java 随机数探秘
- 区块链随机数的实现:墨客随机数子链RandDrop(强随机数和真随机数)_作者陈小虎
- php里的随机数
- [译] 随机数与区块链
- 无法预测的根源——随机数
- 【Android杂谈】生成随机数
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
微信小程序运营与推广完全自学手册
王洪波 / 电子工业出版社 / 2018-6 / 59
本书是运营管理方面的书籍,将小程序的运营推广问题置千小程序的整个运营管理体系中来谈,主要讲述小程序的定位规划、营销吸粉策略、评估优化这三大方面的内容,这三方面的内容之间是三位一体、密切相关的。 书中通过列举丰富且具有代表性的小程序实际案例来向读者提供些可行的运营推广办法。案例涉及美食类、电商类、旅游类、媒体类等小程序,可供多个行业的小程序运营者参考借鉴。 书中所提供的各种小程序营销策略......一起来看看 《微信小程序运营与推广完全自学手册》 这本书的介绍吧!