Do one thing at a time and do well

0%

多线程下ThreadLocalRandom性能分析

  • 对Java中随机数相关的包进行对比分析——Math.randomRandomconcurrent.ThreadLocalRandom
  • 对多线程下 ThreadLocalRandom 的性能进行分析。

Ref

Java中使用随机数

Java中和随机数相关的包,主要包括3个

  1. java.lang.Math.random
  2. java.util.Random
  3. 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 种构造方法

  1. Random(): 创建一个新的随机数生成器,默认当前系统时间的毫秒数作为种子数
  2. Random(long seed): 使用指定的种子创建一个新的随机数生成器
1
2
3
4
5
6
7
8
9
10
11
12
/**
* @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) 的随机数,区间左开右闭。

1
2
3
4
5
//随机返回一个int型整数
int nextInt()

//随机返回一个值在[0,num)的int类型的整数,包括0不包括num
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) // 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] 之间的随机数,可以使用下面代码生成。

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步

  1. 根据老的 seed 生成新的 seed
  2. 由新的 seed 计算出新的随机数

第 2 步的算法是固定的,如果每个线程并发地获取同样的 seed,那么得到的随机数也是一样的。为了避免这种情况,Random 使用 CAS 操作保证每次只有一个线程可以获取并更新 seed,失败的线程则需要自旋重试。

因此,在多线程下用 Random 不太合适,为了解决这个问题,出现了 ThreadLocalRandom,在多线程下,它为每个线程维护一个 seed 变量,这样就不用竞争了。

JMH性能对比测试-ThreadLocalRandom-Random

下面使用 JMH 测试并发情况下,ThreadLocalRandomRandom 的性能差异。

首先,导入如下依赖(JDK9之后自带JMH,不需导入依赖)

1
2
3
4
5
6
7
8
9
10
11
<!--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>

编写测试代码如下

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 {
// 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 表示每秒的吞吐量。

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