一提到 Java 中的随机数,不少人就会想到 Random
,当出现生成随机数这样需求时,大多数人都会选择使用 Random 来生成随机数。Random 类是线程安全的,但其内部使用 CAS 来保证线程安全性,在多线程并发的状况下的时候它的表现是存在优化空间的。在 JDK1.7 以后,Java 提供了更好的解决方案 ThreadLocalRandom,接下来,咱们一块儿探讨下这几个随机数生成器的实现到底有何不一样。算法
Random 这个类是 JDK 提供的用来生成随机数的一个类,这个类并非真正的随机,而是伪随机,伪随机的意思是生成的随机数实际上是有必定规律的,而这个规律出现的周期随着伪随机算法的优劣而不一样,通常来讲周期比较长,可是能够预测。经过下面的代码咱们能够对 Random 进行简单的使用: 安全
Random 中的方法比较多,这里就针对比较常见的 nextInt() 和 nextInt(int bound) 方法进行分析,前者会计算出 int 范围内的随机数,后者若是咱们传入 10,那么他会求出 [0,10) 之间的 int 类型的随机数,左闭右开。咱们首先看一下 Random() 的构造方法: bash
能够发如今构造方法当中,根据当前时间的种子生成了一个 AtomicLong 类型的 seed,这也是咱们后续的关键所在。微信
nextInt() 的代码以下所示:多线程
这个里面直接调用的是 next() 方法,传入的 32,代指的是 Int 类型的位数。并发
这里会根据 seed 当前的值,经过必定的规则(伪随机算法)算出下一个 seed,而后进行 CAS,若是 CAS 失败则继续循环上面的操做。最后根据咱们须要的 bit 位数来进行返回。核心即是 CAS 算法。负载均衡
nextInt(int bound) 的代码以下所示:dom
这个流程比 nextInt() 多了几步,具体步骤以下:ide
在我以前的文章中就有相关的介绍,通常而言,CAS 相比加锁有必定的优点,但并不必定意味着高效。一个马上被想到的解决方案是每次使用 Random 时都去 new 一个新的线程私有化的 Random 对象,或者使用 ThreadLocal 来维护线程私有化对象,但除此以外还存在更高效的方案,下面便来介绍本文的主角 ThreadLocalRandom。性能
在 JDK1.7 以后提供了新的类 ThreadLocalRandom 用来在并发场景下代替 Random。使用方法比较简单:
ThreadLocalRandom.current().nextInt();
ThreadLocalRandom.current().nextInt(10);
复制代码
在 current 方法中有:
能够看见全部的变量都加了 @sun.misc.Contended 这个注解,用来处理伪共享问题。
在 nextInt() 方法当中代码以下:
咱们的关键代码以下:
UNSAFE.putLong(t = Thread.currentThread(), SEED,r=UNSAFE.getLong(t, SEED) + GAMMA);
复制代码
能够看见因为咱们每一个线程各自都维护了种子,这个时候并不须要 CAS,直接进行 put,在这里利用线程之间隔离,减小了并发冲突;相比较 ThreadLocal<Random>
,ThreadLocalRandom 不只仅减小了对象维护的成本,其内部实现也更轻量级。因此 ThreadLocalRandom 性能很高。
除了文章中详细介绍的 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 的方式差距不是很大,位列 二、3 位,共享的 Random 变量则效果最差。
可见,在并发场景下,ThreadLocalRandom 能够明显的提高性能。
注意,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();
}
}
复制代码
梁飞博客中一句话经常在我脑海中萦绕:魔鬼在细节中。优秀的代码都是一个个小细节堆砌出来,今天介绍的 ThreadLocalRandom 也不例外。
在 incubator-dubbo-2.7.0 中,随机负载均衡器的一个小改动即是将 Random 替换为了 ThreadLocalRandom,用于优化并发性能。
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 相关的技术分享。