对Java中随机数相关的包进行对比分析——Math.random
,Random
和 concurrent.ThreadLocalRandom
。
对多线程下 ThreadLocalRandom
的性能进行分析。
Ref
Java中使用随机数 Java中和随机数相关的包,主要包括3个
java.lang.Math.random
java.util.Random
java.util.concurrent.ThreadLocalRandom
java.lang.Math.random Math.random()
方法可以返回区间 [0.0,1.0)
内的 double
型随机数,区间左闭右开。
1 double val = Math.random();
需要注意的是,Math.random()
方法返回是 double
类型,注意取值的范围 [0.0,1.0)
(能够取到零值,注意除零异常)。如果想获取整数类型的随机数,不建议将 val
放大 10 的若干倍然后取整。
更好的解决方案是直接使用 Random
对象的 nextInt
或者 nextLong
方法。
java.util.Random Random()
有 2 种构造方法
Random()
: 创建一个新的随机数生成器,默认当前系统时间的毫秒数作为种子数
Random(long seed)
: 使用指定的种子创建一个新的随机数生成器
1 2 3 4 5 6 7 8 9 10 11 12 public Random(long seed ) { if (getClass() == Random .class ) this.seed = new AtomicLong(initialScramble (seed ) ); else { this.seed = new AtomicLong() ; setSeed(seed ) ; } }
Random
类最常用的方法是 nextInt()
,表示返回一个随机 int
值。该方法可以接受参数,如 nextInt(100)
,表示返回一个区间为 [0,100)
的随机数,区间左开右闭。
1 2 3 4 5 int nextInt () int nextInt (int num)
nextInt(bound)
方法的源码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public int nextInt (int bound) { if (bound <= 0 ) throw new IllegalArgumentException(BadBound); int r = next(31 ); int m = bound - 1 ; if ((bound & m) == 0 ) r = (int )((bound * (long )r) >> 31 ); else { for (int u = r; u - (r = u % bound) + m < 0 ; u = next(31 )) ; } return r; }
[min,max]之间的随机数 [min,max]
之间的随机数,可以使用下面代码生成。
1 2 Random rand = new Random(); int num = rand.nextInt(MAX - MIN + 1 ) + MIN;
seed 对于种子(seed
)相同的 Random
对象,生成的随机数序列是一样的。
1 2 3 4 5 6 7 8 9 private void test7 () { for (int j=0 ;j<5 ;j++){ System.out.println("====第" + j +"次====" ); Random random = new Random(100 ); for (int i=0 ;i<4 ;i++){ System.out.println("index_" + i +":" + random.nextInt(100 )); } } }
执行上述代码,程序输入如下。可以看到对于种子相同的Random
对象,生成的随机数序列是一样的,是一种伪随机数。
伪随机即有规则的随机。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ====第1 次==== index_0:15 index_1:50 index_2:74 index_3:88 ====第2 次==== index_0:15 index_1:50 index_2:74 index_3:88 ====第3 次==== index_0:15 index_1:50 index_2:74 index_3:88 ====第4 次==== index_0:15 index_1:50 index_2:74 index_3:88
java.util.concurrent.ThreadLocalRandom 在多线程下,使用 java.util.Random
获取随机数,它是线程安全的,对于使用指定种子的情况,多个线程会竞争同一 seed
,这会造成性能下降。
这是因为,Random
生成新的随机数需要 2步
根据老的 seed
生成新的 seed
由新的 seed
计算出新的随机数
第 2 步的算法是固定的,如果每个线程并发地获取同样的 seed
,那么得到的随机数也是一样的。为了避免这种情况,Random
使用 CAS
操作保证每次只有一个线程可以获取并更新 seed
,失败的线程则需要自旋重试。
因此,在多线程下用 Random
不太合适,为了解决这个问题,出现了 ThreadLocalRandom
,在多线程下,它为每个线程维护一个 seed
变量,这样就不用竞争了。
JMH性能对比测试-ThreadLocalRandom-Random 下面使用 JMH 测试并发情况下,ThreadLocalRandom
和 Random
的性能差异。
首先,导入如下依赖(JDK9之后自带JMH,不需导入依赖)
1 2 3 4 5 6 7 8 9 10 11 <dependency > <groupId > org.openjdk.jmh</groupId > <artifactId > jmh-core</artifactId > <version > 1.23</version > </dependency > <dependency > <groupId > org.openjdk.jmh</groupId > <artifactId > jmh-generator-annprocess</artifactId > <version > 1.23</version > </dependency >
编写测试代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @BenchmarkMode (Mode.Throughput) @Warmup (iterations = 3 , time = 1 )@Threads (100 )@OutputTimeUnit (TimeUnit.MILLISECONDS)public class SpbAppApplication { public static void main (String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(SpbAppApplication.class.getSimpleName()) .warmupIterations(5 ) .measurementIterations(10 ) .forks(1 ) .build(); new Runner(opt).run(); } @Benchmark public void randomTest () { Random random = new Random(); for (int i = 0 ; i < 10 ; i++) { random.nextInt(10 ); } } @Benchmark public void threadLocalRandomTest () { ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current(); for (int i = 0 ; i < 10 ; i++) { threadLocalRandom.nextInt(10 ); } } }
设定 forks(1)
,设定 @Threads()
分别为 16 和 100,即在一个进程中,分别测试16个线程情况下和100个线程情况下的吞吐量对比数据。
测试结果如下,其中 Cnt
表示运行了多少次,Score
表示执行的成绩,Units
表示每秒的吞吐量。
1 2 3 4 5 6 7 8 9 10 # Threads: 16 threads, will synchronize iterations Benchmark Mode Cnt Score Error Units SpbAppApplication.randomTest thrpt 10 4996.466 ± 3629.543 ops/ms SpbAppApplication.threadLocalRandomTest thrpt 10 37720.715 ± 6566.971 ops/ms # Threads: 100 threads, will synchronize iterations Benchmark Mode Cnt Score Error Units SpbAppApplication.randomTest thrpt 10 3259.461 ± 1819.021 ops/ms SpbAppApplication.threadLocalRandomTest thrpt 10 29948.252 ± 10276.214 ops/ms
从 JMH 测试的结果可以看出,在16个线程并发下,ThreadLocalRandom
在并发情况下的吞吐量约是 Random
的 5 倍。在 100个线程并发下,差距扩大到了将近10倍。
因此在高并发下,尽量使用 ThreadLocalRandom
。
多线程下使用 在每个线程下调用 ThreadLocalRandom.current()
获取对象实例,再调用 nextInt()
方法获取随机数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import java.util.concurrent.ThreadLocalRandom;public class ThreadLocalRandomDemo { public static void main (String[] args) { for (int i = 0 ; i < 10 ; i++) { new Player().start(); } } private static class Player extends Thread { @Override public void run () { System.out.println(getName() + ": " + ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current(); threadLocalRandom.nextInt(100 )); } } }
需要注意的是,多线程下不能把 ThreadLocalRandom.current()
设置为 final
,即下述代码。否则多线程下,产生的随机数是相同的。详情可以参考 多线程下的ThreadLocalRandom用法 | 掘金 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import java.util.concurrent.ThreadLocalRandom;public class ThreadLocalRandomDemo { private static final ThreadLocalRandom RANDOM = ThreadLocalRandom.current(); public static void main (String[] args) { for (int i = 0 ; i < 10 ; i++) { new Player().start(); } } private static class Player extends Thread { @Override public void run () { System.out.println(getName() + ": " + RANDOM.nextInt(100 )); } } }
上述程序的输出如下。
1 2 3 4 5 6 7 8 9 10 Thread-0 : 4 Thread-1 : 4 Thread-2 : 4 Thread-3 : 4 Thread-4 : 4 Thread-5 : 4 Thread-6 : 4 Thread-7 : 4 Thread-8 : 4 Thread-9 : 4