如何生成随机数?在多线程场景下如何高效地生成随机数?

随机数

随机数最重要的特性是它在产生时后面的那个数与前面的那个数毫无关系。
真正的随机数是使用物理现象产生的:比如掷钱币、骰子、转轮、使用电子组件的噪音、核裂变等等。这样的随机数生成器叫做物理性随机数生成器,它们的缺点是技术要求比较高。
在实际应用中往往使用伪随机数就足够了。这些数列是“似乎”随机的数,实际上它们是通过一个固定的、可以重复的计算方法产生的。它们不真正地随机,因为它们实际上是可以计算出来的,但是它们具有类似于随机数的统计特征。这样的生成器叫做伪随机数生成器。

举例说明

以下示例代码(生成 10 以内的随机整数)包含了常用的生成随机数的用法。

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package io.github.youmaycallmev.random;

import java.security.SecureRandom;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

public class RandomTest {

private static final Random RANDOM = new Random();

/** 固定seed的Random */
private static final Random RANDOM_FIXED_SEED = new Random(10);

/** 固定seed的Random */
private static final Random RANDOM_FIXED_SEED2 = new Random(10);

public static void main(String[] args) {
long start = System.currentTimeMillis();
byRandom();
System.out.println("cost: " + (System.currentTimeMillis() - start));

start = System.currentTimeMillis();
byRandomFixedSeed();
System.out.println("byRandomFixedSeed cost: " + (System.currentTimeMillis() - start));

start = System.currentTimeMillis();
byMath();
System.out.println("byMath cost: " + (System.currentTimeMillis() - start));

start = System.currentTimeMillis();
byThreadLocalRandom();
System.out.println("byThreadLocalRandom cost: " + (System.currentTimeMillis() - start));

start = System.currentTimeMillis();
bySecureRandom();
System.out.println("bySecureRandom cost: " + (System.currentTimeMillis() - start));
}

public static void byRandom() {
for (int i = 0; i < 10; i++) {
System.out.print(RANDOM.nextInt(10) + "\t");
}
System.out.println();
}

public static void byRandomFixedSeed() {
for (int i = 0; i < 10; i++) {
System.out.print(RANDOM_FIXED_SEED.nextInt(10) + "\t");
}
System.out.println();
for (int i = 0; i < 10; i++) {
System.out.print(RANDOM_FIXED_SEED2.nextInt(10) + "\t");
}
System.out.println();
}

public static void byMath() {
for (int i = 0; i < 10; i++) {
System.out.print((int) (Math.random() * 11) + "\t");
}
System.out.println();
}

public static void byThreadLocalRandom() {
for (int i = 0; i < 10; i++) {
System.out.print(ThreadLocalRandom.current().nextInt(10) + "\t");
}
System.out.println();
}

public static void bySecureRandom() {
SecureRandom secureRandom = new SecureRandom();
for (int i = 0; i < 10; i++) {
System.out.print(secureRandom.nextInt(10) + "\t");
}
System.out.println();
}
}

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
1	9	0	8	8	6	8	7	3	4	
cost: 1
3 0 3 0 6 6 7 8 1 4
3 0 3 0 6 6 7 8 1 4
byRandomFixedSeed cost: 0
1 9 7 4 9 1 8 0 7 5
byMath cost: 1
9 0 9 8 0 7 9 7 6 0
byThreadLocalRandom cost: 1
4 7 5 5 8 1 4 5 4 9
bySecureRandom cost: 501

通过运行结果我们发现,固定 seed 的为 10 的 Random 生成的随机数是一样的。SecureRandom 生成 10 个随机数需要的时间最长。

原理分析

Random 类

Random 类提供了两个构造方法:一个不带参数的构造方法,一个带 seed 参数的构造方法。默认的构造方法其初始化的源码(解析请看注释)如下:

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
/**
* 无参构造方法
*/
public Random() {
// 按位异或运算
this(seedUniquifier() ^ System.nanoTime());
}

/**
* 使用无锁算法产生唯一的种子
*
*/
private static long seedUniquifier() {
// L'Ecuyer, "Tables of Linear Congruential Generators of
// Different Sizes and Good Lattice Structure", 1999
for (;;) {
// 获取 seedUniquifier 的当前值 current
long current = seedUniquifier.get();
// 当前值和 181783497276652981L 进行乘法运算,并得到结果 next
long next = current * 181783497276652981L;
// 如果当前值 current 和运算结果 next 不等则修改 seedUniquifier 值为运算结果 next
if (seedUniquifier.compareAndSet(current, next))
// 返回种子 next
return next;
}
}

/** 初始值为 8682522807148012L 的 Long 原子类 */
private static final AtomicLong seedUniquifier
= new AtomicLong(8682522807148012L);

接下来,我们再看看有参数的构造方法源码(解析请看注释):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 带有参数的构造方法
*/
public Random(long seed) {
// 使用的就是 Random 类
if (getClass() == Random.class)
this.seed = new AtomicLong(initialScramble(seed));
else {
// subclass might have overriden setSeed(如果使用的是 Random 的子类,有可能重写了 setSeed 方法)
this.seed = new AtomicLong();
// 保存 seed
setSeed(seed);
}
}

private static long initialScramble(long seed) {
// 先异或再按位与
return (seed ^ multiplier) & mask;
}

private static final long multiplier = 0x5DEECE66DL;

/** 转换为二进制就是 48 个 1 */
private static final long mask = (1L << 48) - 1;

然后我们再看看是如何生成随机数的:

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
public int nextInt(int bound) {
// 如果边界值小于等于 0
if (bound <= 0)
// 异常
throw new IllegalArgumentException(BadBound);
// 调用 next 方法生成随机数
int r = next(31);
// 边界值减去 1
int m = bound - 1;
// 如果相与的结果为 0,说明边界值为 2 的幂次方
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;
}

protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
// 保存原来的 seed
oldseed = seed.get();
// 生成新 seed
nextseed = (oldseed * multiplier + addend) & mask;
// 无锁算法:比较交换 seed 的值为 nextseed
} while (!seed.compareAndSet(oldseed, nextseed));
// 无符号右移 17 位
return (int)(nextseed >>> (48 - bits));
}

private static final long addend = 0xBL;

通过以上代码的简单分析,我们可以知道随机数的生成是通过一定的算法算出来的,而且和 seed 有非常紧密的关系:算法是固定的,如果 seed 也固定了,就会造成上述测试结果中那样,生成的随机数完全一致的情况。如果能够“预测”seed 的值(默认的 seed 也是通过算法得到的),就有可能生成一样的随机数,说明 Random 类的安全性还有待提升。

接下来,我们再看看几段 JDK 1.8 官方文档的说明:

An instance of this class is used to generate a stream of pseudorandom numbers. The class uses a 48-bit seed, which is modified using a linear congruential formula. (See Donald Knuth, The Art of Computer Programming, Volume 2, Section 3.2.1.)

该类的实例用于生成伪随机数流。该类使用 48 位种子,使用线性同余公式进行修改。 (参见 Donald Knuth,计算机程序设计的艺术,第 2 卷,第 3.2.1 节。)

If two instances of Random are created with the same seed, and the same sequence of method calls is made for each, they will generate and return identical sequences of numbers.

如果使用相同的种子创建两个 Random 实例,并且为每个实例创建相同的方法调用序列,则它们将生成并返回相同的数字序列。

Instances of java.util.Random are threadsafe. However, the concurrent use of the same java.util.Random instance across threads may encounter contention and consequent poor performance. Consider instead using java.util.concurrent.ThreadLocalRandom in multithreaded designs.

java.util.Random 的实例是线程安全的。但是,跨线程并发使用相同的 java.util.Random 实例可能会遇到争用,从而导致性能不佳。请考虑在多线程设计中使用 java.util.concurrent.ThreadLocalRandom。
PS:这里的竞争,主要还是对 seed 的竞争,在多线程的调用下,next(int bits) 方法中对 seed 的修改并不能一次就修改成功,从而一直循环,直到修改成功。

Instances of java.util.Random are not cryptographically secure. Consider instead using java.security.SecureRandom to get a cryptographically secure pseudo-random number generator for use by security-sensitive applications.

java.util.Random 的实例不具有加密安全性。如果需要,请考虑使用 java.security.SecureRandom 来获取加密安全的伪随机数生成器,以供安全敏感的应用程序使用。

Math

我们在上面了解了 Random 类,再来看 Math 工具类就比较简单了。在 Math 类里面有一个如下的私有的静态内部类:

1
2
3
4
5
6
public static double random() {
return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
}
private static final class RandomNumberGeneratorHolder {
static final Random randomNumberGenerator = new Random();
}

对!里面就是一个静态的类成员变量 Random。生成随机数就是调用 Random 类的 nextDouble 方法。

ThreadLocalRandom

ThreadLocalRandom 作为 Random 的子类,重写了 Random 里面的方法,并对并发做了管理和维护,所以在多线程场景下表现比 Random 更好。

SecureRandom

SecureRandom 作为 Random 的子类,该类提供加密强随机数生成器,所以在性能上比 Random 就略逊一筹。

小结

如果没有多线程的场景,那就用 Math 来生成随机数吧,毕竟提供现成的工具类,不用自己维护实例。多线程场景,还是使用 ThreadLocalRandom。如果对安全性有一定要求,可以使用 SecureRandom,那么在性能上有一点点的影响。
通过对 Random 源码的部分解析,其中有 3 个彩蛋可以重点记一下:

  1. 如何生成指定位数全为 1 二进制数?
  2. 如何判断一个数是不是 2 的幂次方?
  3. 什么是无锁算法(CAS)?