Java并发编程学习——Java并发编程之美读书笔记三

Random类及其局限性

在JDK7之前包括现在Random都是使用比较广泛的随机数生成工具。在java.lang.Math中随机数生成也是使用的java.util.Random的实例。

下面是Random的一种常见的使用方式。

基本步骤就是生成一个Random实例,然后通过这个实例的方法去生成随机数字。

1
2
3
4
5
6
7
8
9
10
public class RandomTest {

public static void main(String[] args) {
Random random = new Random();
for (int i = 0; i < 5; i++) {
System.out.println(random.nextInt(10));
}
}

}

随机数的生成需要一个默认的种子,这个种子是一个long类型的数字。我们可以查看一下Random的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 无参构造函数其实是通过当前时间生成long类型的种子的
public Random() {
// 调用的是有参构造函数
this(seedUniquifier() ^ System.nanoTime());
}
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);
}
}

我们来看一下nextInt()方法的源码

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
// 无参nextInt方法调用了next方法
public int nextInt() {
return next(32);
}

protected int next(int bits) {
long oldseed, nextseed;
// 获取当前的seed
AtomicLong seed = this.seed;
do {
oldseed = seed.get();
// 通过旧种子生成新种子
nextseed = (oldseed * multiplier + addend) & mask;
// CAS操作更新种子
} while (!seed.compareAndSet(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}

public int nextInt(int bound) {
// 参数校验
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
// 还是调用的next方法
// 后面通过计算控制范围
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;
}

看了以上的代码,我们先不管种子是否是原子变量,如果多个线程去调用这个随机方法获得种子然后生成随机数,因为方法里新种子的生成依赖于旧种子,而旧种子是存放在共享变量里的,这里就会导致线程不安全问题,如果线程1生成一个种子,然后线程2和线程3同时调用该方法然后生成新种子,这个时候两个线程调用的旧种子是一样的,又因为旧种子变成新种子的算法是固定的,所以这两个线程得到的是同一个新种子,那么就意味着他们会生成同样的随机数。

而我们要注意的是在next方法里seed是被声明成AtomicLong类型的,它是原子变量,所以这样就可以解决线程安全的问题了(同一时刻只有一个线程能对这个原子变量进行操作),后面原子变量的更新操作使用的是CAS操作,同一时刻只有一个线程能更新成功,这样就会 导致大量线程自旋重试 ,这样就极大地降低了并发性能。

ThreadLocalRandom

ThreadLocalRandom很好的解决了Random在高并发场景下的缺陷和不足。与ThreadLocal的原理一样ThreadLocalRandom使用的也是 线程封闭技术

使用方式和Random差不多。

1
2
3
4
5
6
7
8
public class RandomTest {
public static void main(String[] args) {
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
for (int i = 0; i < 10; i++) {
System.out.println(threadLocalRandom.nextInt(10));
}
}
}

因为在Random中种子是共享变量,所以在多线程环境下会出现线程安全问题。而ThreadLocalRandom则是把种子变为线程本地变量。这样每个线程就会通过自己线程里的旧种子去更新种子。

ThreadLocalRandom源码分析

ThreadLocalRandom源码

我们可以发现ThreadLocalRandom是继承了Random类的,但是需要注意的是ThreadLocalRandom并没有使用Random的seed变量,具体的变量存放在Thread中的ThreadLocalRandomSeed中(存放在线程中)。当调用ThreadLocalRandom的nextInt方法的时候,会获取当前线程的ThreadLocalRandomSeed变量并通过这个种子更新种子然后使用新种子来随机生成数字

其中ThreadLocalRandom中的seeder和probeGenerator是两个原子性变量,在初始化调用线程的种子和探针的时候会用到他们,每个线程只会调用一次

另外instance是ThreadLocalRandom的一个实例而且是static的,也就是说多个线程获取的是同一个实例,但是因为种子是存放在线程中的,所以不会产生安全问题。

Unsafe机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static final sun.misc.Unsafe UNSAFE;
private static final long SEED;
private static final long PROBE;
private static final long SECONDARY;
static {
try {
// 获取Unsafe实例
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> tk = Thread.class;
// 获取threadLocalRandomSeed,threadLocalRandomProbe,
// threadLocalRandomSecondarySeed在Thread中的偏移量
SEED = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSeed"));
PROBE = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomProbe"));
SECONDARY = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSecondarySeed"));
} catch (Exception e) {
throw new Error(e);
}
}

ThreadLocalRandom.current()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static ThreadLocalRandom current() {
// 判断是否为第一次调用,为0则为第一次调用,如果是做初始化操作
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
// 返回统一的static实例
return instance;
}
// 初始化当前线程的种子变量
static final void localInit() {
// 初始化探针
int p = probeGenerator.addAndGet(PROBE_INCREMENT);
int probe = (p == 0) ? 1 : p; // skip 0
// 初始化seed
long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
Thread t = Thread.currentThread();
UNSAFE.putLong(t, SEED, seed);
UNSAFE.putInt(t, PROBE, probe);
}

int nextInt(int bound)方法

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
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 two
r &= m;
else { // reject over-represented candidates
for (int u = r >>> 1;
u + m - (r = u % bound) < 0;
u = mix32(nextSeed()) >>> 1)
;
}
return r;
}

final long nextSeed() {
Thread t; long r; // read and update per-thread seed
// 更新种子
UNSAFE.putLong(t = Thread.currentThread(), SEED,
// 这里获取线程种子并进行 + GAMMA操作
r = UNSAFE.getLong(t, SEED) + GAMMA);
return r;
}

总结

因为Random的种子生成随机数的方法,在Random中的种子是共享的所以多线程会出现并发问题,而Random中将种子声明成原子变量并且使用CAS更新会导致在多线程环境下多个线程去竞争资源,从而导致大量线程自旋,浪费资源和降低并发能力。

在ThreadLocalRandom中使用了线程封闭技术来解决这个问题,线程封闭即使线程本地化,将共享变量进行本地化,从而避免了线程安全问题和提高了并发能力。

-------------本文结束感谢阅读-------------