转载自   Java中随机数的原理,以及使用时的注意点

1 前言

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

2 Random

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

public class RandomDemo {public static void main(String[] args) {Random rand = new Random();System.out.println(rand.nextInt());System.out.println(rand.nextInt(10));}
}

Random原理

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

public Random() {// 默认构造方法传入的seed值, // 这个值由种子算法得到的一个唯一值与纳秒值通过位运算得到// 尽可能做到了和另一个构造方法的seed值的不同this(seedUniquifier() ^ System.nanoTime());
}public Random(long seed) {if (getClass() == Random.class)this.seed = new AtomicLong(initialScramble(seed));else {// subclass might have overriden setSeedthis.seed = new AtomicLong();setSeed(seed);}
}private static long seedUniquifier() {// L'Ecuyer, "Tables of Linear Congruential Generators of// Different Sizes and Good Lattice Structure", 1999for (;;) {long current = seedUniquifier.get();long next = current * 181783497276652981L;if (seedUniquifier.compareAndSet(current, next))return next;}
}private static final AtomicLong seedUniquifier = new AtomicLong(8682522807148012L);

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

nextInt()

nextInt() 的代码如下所示:

public int nextInt() {return next(32);
}

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

protected int next(int bits) {long oldseed, nextseed;AtomicLong seed = this.seed;do {oldseed = seed.get();// 根据一定的规则生成nextseednextseed = (oldseed * multiplier + addend) & mask;// 更新oldseed的值,通过cas保证线程安全} while (!seed.compareAndSet(oldseed, nextseed));// 返回前还需要位运算return (int)(nextseed >>> (48 - bits));
}

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

nextInt(int bound)

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

public int nextInt(int bound) {if (bound <= 0)throw new IllegalArgumentException(BadBound);int r = next(31);int m = bound - 1;if ((bound & m) == 0)  // i.e., bound is a power of 2r = (int)((bound * (long)r) >> 31);else {for (int u = r;u - (r = u % bound) + m < 0;u = next(31));}return r;
}

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

  1. 首先获取 31 位的随机数,注意这里是 31 位,和上面 32 位不同,因为在 nextInt() 方法中可以获取到随机数可能是负数,而 nextInt(int bound) 规定只能获取到 [0,bound) 之前的随机数,也就意味着必须是正数,预留一位符号位,所以只获取了31位。(不要想着使用取绝对值这样操作,会导致性能下降)
  2. 然后进行取 bound 操作。
  3. 如果 bound 是2的幂次方,可以直接将第一步获取的值乘以 bound 然后右移31位,解释一下:如果 bound 是4,那么乘以4其实就是左移2位,其实就是变成了33位,再右移31位的话,就又会变成2位,最后,2位 int 的范围其实就是 [0,4) 了。
  4. 如果不是 2 的幂,通过模运算进行处理。

并发瓶颈

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

3 ThreadLocalRandom

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

public class RandomTest {public static void main(String[] args) {System.out.println(ThreadLocalRandom.current().nextInt());System.out.println(ThreadLocalRandom.current().nextInt());System.out.println(ThreadLocalRandom.current().nextInt(10));}
}

在 current 方法中有:

public static ThreadLocalRandom current() {if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)// 判断当前线程是否初始化, 如果没有则初始化localInit();return instance;
}static final void localInit() {int p = probeGenerator.addAndGet(PROBE_INCREMENT);int probe = (p == 0) ? 1 : p; // skip 0long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));Thread t = Thread.currentThread();UNSAFE.putLong(t, SEED, seed);UNSAFE.putInt(t, PROBE, probe);
}

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

  • threadLocalRandomSeed:ThreadLocalRandom 使用它来控制随机数种子。
  • threadLocalRandomProbe:ThreadLocalRandom 使用它来控制初始化。
  • threadLocalRandomSecondarySeed:二级种子。

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

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

public int nextInt() {return mix32(nextSeed());
}final long nextSeed() {Thread t; long r; // read and update per-thread seedUNSAFE.putLong(t = Thread.currentThread(), SEED,r = UNSAFE.getLong(t, SEED) + GAMMA);return r;
}

我们的关键代码如下:

  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 测评。

@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);@Benchmarkpublic int random() {return random.nextInt();}@Benchmarkpublic int threadLocalRandom() {return ThreadLocalRandom.current().nextInt();}@Benchmarkpublic int threadLocalRandomHolder() {return threadLocalRandomHolder.get().nextInt();}@Benchmarkpublic 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 BadCase {public static void main(String[] args) {ThreadLocalRandom rand = ThreadLocalRandom.current();for (int i = 0; i < 10; i++) {new Thread(() -> {System.out.println(rand.nextInt());}).start();}}
}

输出相同的随机数:

-1667209487

-1667209487

-1667209487

-1667209487

-1667209487

-1667209487

-1667209487

-1667209487

-1667209487

-1667209487

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

public class GoodCase {public static void main(String[] args) {for (int i = 0; i < 3; i++) {new Thread(() -> {System.out.println(ThreadLocalRandom.current().nextInt());System.out.println(ThreadLocalRandom.current().nextInt());System.out.println(ThreadLocalRandom.current().nextInt());}).start();}}
}

彩蛋1

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

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

彩蛋2

ThreadLocalRandom 的 nextInt(int bound) 方法中,当 bound 不为 2 的幂次方时,进入 else 分支,使用了一个循环来修改 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 twor &= m;else { // reject over-represented candidatesfor (int u = r >>> 1;u + m - (r = u % bound) < 0;u = mix32(nextSeed()) >>> 1);}return r;}

Java中随机数的原理,以及使用时的注意点相关推荐

  1. Java中随机数的产生

    Java中随机数的产生有两种方法: 一.利用Random类的实例对象产生: Random r = new Random(); int i =r.nextInt(99)+1;   //产生1-100之间 ...

  2. java对象赋值_Java 对象不使用时为什么要赋值为 null?

    前言 许多Java开发者都曾听说过"不使用的对象应手动赋值为null"这句话,而且好多开发者一直信奉着这句话:问其原因,大都是回答"有利于GC更早回收内存,减少内存占用& ...

  3. Java中数据库连接池原理机制的详细讲解

    连接池的基本工作原理 1.基本概念及原理 由上面的分析可以看出,问题的根源就在于对数据库连接资源的低效管理.我们知道,对于共享资源,有一个很著名的设计模式:资源池(ResourcePool).该模式正 ...

  4. Java篇 - 随机数的原理、伪随机和优化

    这篇来说说Java中的随机数,以及为什么说随机数是伪随机. 目录: Math.random() Random类 伪随机 如何优化随机 封装的一个随机处理工具类 1. Math.random() 1.1 ...

  5. java中JVM的原理【转】

    一.java虚拟机的生命周期: Java虚拟机的生命周期 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序.程序开始执行时他才运行,程序结束时他就停止.你在同一台机器上运行三个程序,就会 ...

  6. Java中的锁原理、锁优化、CAS、AQS详解

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:景小财 www.jianshu.com/p/e674ee68 ...

  7. Java中的锁[原理、锁优化、CAS、AQS]

    点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:用好Java中的枚举,真的没有那么简单!个人原创+1博客:点击前往,查看更多 作者:高广超 链接:https:/ ...

  8. Java中的锁原理、锁优化、CAS、AQS详解!

    阅读本文大概需要 2.8 分钟. 来源:jianshu.com/p/e674ee68fd3f 一.为什么要用锁? 锁-是为了解决并发操作引起的脏读.数据不一致的问题. 二.锁实现的基本原理 2.1.v ...

  9. java中JVM的原理

    一.Java虚拟机的生命周期: Java虚拟机的生命周期 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序.程序开始执行时他才运行,程序结束时他就停止.你在同一台机器上运行三个程序,就会 ...

最新文章

  1. 噪声dba是什么单位_在职DBA : 工作多年为什么还是选择报读工商管理博士
  2. 阿姨帮万勇:O2O产品的颠覆与布局,阿姨帮未来发展战略独家披露
  3. Java多线程-生产者与消费者
  4. Docker Compose编排(写法格式及实验)
  5. Sun为何一掷千金拿下MySQL?
  6. vue 获取当前元素的父元素_react获取触发元素的属性 e.target.dataset
  7. python爬虫短片_Python爬虫练习:爬取全民小视频(附代码,过程)
  8. R语言中package ‘xxxx’ is not available (for R version 3.2.5)解决
  9. 人受失败后多久可以做第二次_做完皮秒多久可以用自己的护肤产品、过来人分享皮秒后怎么护肤?...
  10. 想开发一个背单词的学习软件
  11. JMeter集合点功能的使用
  12. spring、mybatis、测试用例执行后数据没有入库
  13. 配置vivado用vscode编辑文本
  14. 什么是透明、匿名、高匿代理?详解!
  15. 用JavaScript写的U校园自动答题浏览器分析
  16. H.264(MPEG)-4AVC
  17. 移植wpa_supplicant-2.2
  18. 计算机论文的研究思路与方法,硕士论文中研究方法怎么写 介绍3种简单的方法...
  19. php 分换算成元,千元换算成元(千元单位换算器)
  20. Spark多版本共存

热门文章

  1. 7-2 最长公共子序列 (10 分)(思路加详解)
  2. vue3的传送门teleport究竟有多神奇?suspense发起异步请求有多简约?
  3. php去掉多字节字符,PHP 面试题 - 如果没有 mb 系列函数,如何切割多字节字符串...
  4. java servlet jsp javabean关系图_Servlet+JSP+JavaBean开发模式(MVC)介绍
  5. 蓝桥杯2017初赛-k倍区间-前缀和
  6. 01背包+概率问题 计蒜客 offer
  7. python 语音播放_基于Python编写的语音播放软件
  8. android仿支付宝弹窗,【转】MUI自定义底部弹窗自带遮罩层仿支付宝支付弹窗
  9. Shader 坐标转换
  10. 【BZOJ 4671】异或图 【斯特林反演】【线性基】【贝尔数复杂度】