揭秘Java高效随机数生成器

1.前言

在Java中一提到随机数,不少人就会想到Ramdom类,若是有生成随机数的需求的时候,大多数时候都会选择使用Random来进行随机数生成,虽然其内部使用CAS来实现,可是在多线程并发的状况下的时候它的表现并非很好。在JDK1.7以后,JDK提供了提供了更好的解决方案,接下来让咱们一块儿学习下到底为何Ramdom会慢?又是怎么解决的呢?java

2.Random

Random这个类是JDK提供的用来生成随机数的一个类,这个类并非真正的随机,而是伪随机,伪随机的意思是生成的随机数实际上是有必定规律的,而这个规律出现的周期随着伪随机算法的优劣而不一样,通常来讲周期比较长,可是能够预测。经过下面的代码咱们能够对Random进行简单的使用: git

2.1Random原理

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

能够看见在构造方法当中根据当前时间的种子生成了一个AtomicLong类型的seed,这也是咱们后续的关键所在。面试

2.1.1 nextInt()

在nextInt()中代码以下: 算法

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

这里会根据seed当前的值,经过必定的规则(伪随机)算出下一个seed,而后进行cas,若是cas失败继续循环上面的操做。最后根据咱们须要的bit位数来进行返回。bash

2.1.2 nextInt(int bound)

在nextInt(int bound)中代码以下: 多线程

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

  1. 首先获取31位的随机数,注意这里是31位,和上面32位不一样,由于在nextInt()方法中能够获取到负数的随机数,而nextInt(int bound)规定只能获取到[0,bound)以前的随机数,也就是必须是正数,而int的第一位是符号位因此只获取了31位。
  2. 而后进行取bound操做。
  3. 若是bound是2的幂,那么直接将第一步获取的数据乘以bound而后右移31位,解释一下:若是bound是4那么,若是乘以4其实就是左移2位,那么其实就是变成了33位,那么再右移31位的话,就又会变成2位,那么2位的int的大小范围其实就是[0,4)了。
  4. 若是不是2的幂,经过取余的操做进行处理。

2.1.3 并发瓶颈

CAS: 能够看见在next(int bits)方法中,对AtomicLong进行CAS操做,若是失败则会对其进行循环重试。不少人一看见CAS,由于其不须要加锁,因此立刻就想到高性能,高并发。可是在这里,他却成为了咱们多线程并发性能的瓶颈,能够想象当咱们多个线程都进行CAS的时候一定只有一个失败其余的继续会循环作CAS操做,当并发线程越多的时候,其性能确定越低。dom

伪共享:有关于伪共享和缓存行的描述能够看个人你应该知道的高性能无锁队列Disruptor,对于AtomicLong中的value并无处理缓存行

3.ThreadLocalRandom

在JDK1.7以后提供了新的类ThreadLocalRandom用来代替Random。使用方法比较简单:

在current方法中有:

能够看见若是没有初始化会对其进行初始化,而这里咱们的seed再也不是一个全局变量,在咱们的Thread中有三个变量:

  • threadLocalRandomSeed:这个是咱们用来控制随机数的种子。
  • threadLocalRandomProbe:这个是ThreadLocalRandom用来控制初始化。
  • threadLocalRandomSecondarySeed:这个是二级种子。

能够看见全部的变量都加了@sun.misc.Contended这个注解,这个是用来处理伪共享的问题。

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

咱们的关键代码以下:

UNSAFE.putLong(t = Thread.currentThread(), SEED,r=UNSAFE.getLong(t, SEED) + GAMMA);
复制代码

能够看见因为咱们每一个线程各自都维护了种子,这个时候并不须要CAS,直接进行put,在这里利用线程之间隔离,减小了并发冲突,因此ThreadLocalRandom性能很高。

4.性能数据

使用JMH进行基准测试:

@BenchmarkMode({Mode.AverageTime})
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations=3, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations=3,time = 5)
@Threads(4)
@Fork(1)
@State(Scope.Benchmark)
public class Myclass {
   Random random = new Random();
   ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();


   @Benchmark
   public int measureRandom(){
       return random.nextInt();
   }
   @Benchmark
   public int threadLocalmeasureRandom(){
       return threadLocalRandom.nextInt();
   }
}
复制代码
并发线程 Random ThreadLocalRandom
1 12.798 ns/op 4.690 ns/op
4 361.027 ns/op 5.930 ns/op
16 2288.391 ns/op 22.155 ns/op
32 4812.740 ns/op 49.144 ns/op

能够看见ThreadLocalRandom 基本上是完虐Random,并发程度越高差距越大。

最后

相信读完这篇文章之后,将来若是在实际应用中使用随机数你确定会有新的选择。

最后这篇文章被我收录于JGrowing,一个全面,优秀,由社区一块儿共建的Java学习路线,若是您想参与开源项目的维护,能够一块儿共建,github地址为:github.com/javagrowing… 麻烦给个小星星哟。

若是你以为这篇文章对你有文章,能够关注个人技术公众号,最近做者收集了不少最新的学习资料视频以及面试资料,关注以后便可领取,你的关注和转发是对我最大的支持,O(∩_∩)O

相关文章
相关标签/搜索