参考资料
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
型随机数,区间左闭右开。
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)
: 使用指定的种子创建一个新的随机数生成器
/**
* @param seed the initial seed
*/
public Random(long seed) {
if (getClass() == Random.class)
this.seed = new AtomicLong(initialScramble(seed));
else {
// subclass might have overriden setSeed
this.seed = new AtomicLong();
setSeed(seed);
}
}
Random
类最常用的方法是 nextInt()
,表示返回一个随机 int
值。该方法可以接受参数,如 nextInt(100)
,表示返回一个区间为 [0,100)
的随机数,区间左闭右开。
//随机返回一个int型整数
int nextInt()
//随机返回一个值在[0,num)的int类型的整数,包括0不包括num
int nextInt(int num)
nextInt(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 2
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]
之间的随机数,可以使用下面代码生成。
Random rand = new Random();
int num = rand.nextInt(MAX - MIN + 1) + MIN;
seed
对于种子(seed
)相同的 Random
对象,生成的随机数序列是一样的。
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次====
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
ThreadLocalRandom为每个线程维护一个种子seed
在多线程下,使用 java.util.Random
获取随机数,它是线程安全的。但深挖 Random 的实现过程,会发现多个线程会竞争同一 seed
,这会造成性能下降。
这是因为,Random
生成新的随机数需要 2 步
- 根据老的
seed
生成新的seed
- 由新的
seed
计算出新的随机数
第 2 步的算法是固定的,如果每个线程并发地获取同样的 seed
,那么得到的随机数也是一样的。为了避免这种情况,Random
使用 CAS
操作保证每次只有一个线程可以获取并更新 seed
,失败的线程则需要自旋重试。
因此,在多线程下用 Random
不太合适,为了解决这个问题,出现了 ThreadLocalRandom
,在多线程下,它为每个线程维护一个 seed
变量,这样就不用竞争了。
JMH性能对比测试-ThreadLocalRandom-Random
下面使用 JMH 测试并发情况下,ThreadLocalRandom
和 Random
的性能差异。
首先,导入如下依赖(JDK9之后自带JMH,不需导入依赖)
<!--JMH-->
<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>
编写测试代码如下
@BenchmarkMode(Mode.Throughput) // 测试类型:吞吐量
@Warmup(iterations = 3, time = 1)
@Threads(100)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class SpbAppApplication {
public static void main(String[] args) throws RunnerException {
// SpringApplication.run(SpbAppApplication.class, args);
// 启动基准测试
Options opt = new OptionsBuilder()
.include(SpbAppApplication.class.getSimpleName()) // 要导入的测试类
.warmupIterations(5) // 预热 5 轮
.measurementIterations(10) // 度量10轮
.forks(1)
.build();
new Runner(opt).run(); // 执行测试
}
/**
* Random 性能测试
*/
@Benchmark
public void randomTest() {
Random random = new Random();
for (int i = 0; i < 10; i++) {
// 生成 0-9 的随机数
random.nextInt(10);
}
}
/**
* ThreadLocalRandom 性能测试
*/
@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
表示每秒的吞吐量。
# 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()
方法获取随机数。
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用法 | 掘金。
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));
}
}
}
上述程序的输出如下。
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