前言

学习 ThreadLocalRandom 的时候遇到一些疑惑,为何使用它在多线程下会产生相同的随机数?

阅读源码后终于稍微了解了一些它的运行机制,总结出它在多线程下正确的用法,特此记录。

ThreadLocalRandom的用处

在多线程下,使用 java.util.Random 产生的实例来产生随机数是线程安全的,但深挖 Random 的实现过程,会发现多个线程会竞争同一 seed 而造成性能降低。

其原因在于:

Random 生成新的随机数需要两步:

  • 根据老的 seed 生成新的 seed

  • 由新的 seed 计算出新的随机数

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

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

但是我在使用的时候,发现 ThreadLocalRandom 在多线程下产生了相同的随机数,这是怎么回事呢?

ThreadLocalRandom多线程下产生相同随机数

来看一下产生相同随机数的示例代码:

 1 import java.util.concurrent.ThreadLocalRandom;
 2 
 3 public class ThreadLocalRandomDemo {
 4 
 5     private static final ThreadLocalRandom RANDOM =
 6             ThreadLocalRandom.current();
 7 
 8     public static void main(String[] args) {
 9         for (int i = 0; i < 10; i++) {
10             new Player().start();
11         }
12     }
13 
14     private static class Player extends Thread {
15         @Override
16         public void run() {
17             System.out.println(getName() + ": " + RANDOM.nextInt(100));
18         }
19     }
20 }

运行该代码,结果如下:

 1 Thread-0: 4
 2 Thread-1: 4
 3 Thread-2: 4
 4 Thread-3: 4
 5 Thread-4: 4
 6 Thread-5: 4
 7 Thread-6: 4
 8 Thread-7: 4
 9 Thread-8: 4
10 Thread-9: 4

为此,我阅读了 ThreadLocalRandom 的源码,从中找到了端倪。

先是静态 current() 方法:

1 public static ThreadLocalRandom current() {
2     //如果线程第一次调用 current() 方法,执行 localInit()方法
3     if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
4         localInit();
5     return instance;
6 }

初始化方法 localInit()中,为线程初始化了 seed,并保存在 UNSAFE 里,这里 UNSAFE 的方法是 native 方法,我不太了解,但并不影响理解。可以把这里的操作看作是初始化了 seed,把线程和 seed 以键值对的形式保存起来。

1 static final void localInit() {
2     int p = probeGenerator.addAndGet(PROBE_INCREMENT);
3     int probe = (p == 0) ? 1 : p; // skip 0
4     long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
5     Thread t = Thread.currentThread();
6     UNSAFE.putLong(t, SEED, seed);
7     UNSAFE.putInt(t, PROBE, probe);
8 }

当要生成随机数的时候,调用 nextInt() 方法:

 1 public int nextInt(int bound) {
 2     if (bound <= 0)
 3         throw new IllegalArgumentException(BadBound);
 4     //第一处
 5     int r = mix32(nextSeed());
 6     int m = bound - 1;
 7     if ((bound & m) == 0) // power of two
 8         r &= m;
 9     else { // reject over-represented candidates
10         for (int u = r >>> 1;
11              u + m - (r = u % bound) < 0;
12              u = mix32(nextSeed()) >>> 1)
13             ;
14     }
15     return r;
16 }

这里主要关注 第一处nextSeed() 方法:

1 final long nextSeed() {
2     Thread t; long r; // read and update per-thread seed
3     UNSAFE.putLong(t = Thread.currentThread(), SEED,
4                    r = UNSAFE.getLong(t, SEED) + GAMMA);
5     return r;
6 }

好了,问题来了!这里返回的值是 r = UNSAFE.getLong(t, SEED) + GAMMA,是从 UNSAFE 里取出来的。但问题是,这里取出来的值对不对?或者说,能否取出来?

回到示例代码,我们在主线程调用了 TreadLocalRandomcurrent() 方法,该方法把主线程和主线程的 seed 存入了 UNSAFE

接下来,我们在非主线程调用 nextInt(),但非主线程和 seed 的键值对之前并没有存入 UNSAFE 。但我们却从 UNSAFE 里取非主线程的 seed 值,虽然我不知道取出来的 seed 到底是什么,但肯定不是多线程下想要的结果,而这也导致了多线程下产生的随机数是重复的。

那么在多线程下如何正确地使用 ThreadLocalRandom 呢?

ThreadLocalRandom多线程下正确用法

结合上述分析,正确地使用 ThreadLocalRandom,肯定需要给每个线程初始化一个 seed,那就需要调用 ThreadLocalRandom.current() 方法。

那么有个疑问,在每个线程里都调用 ThreadLocalRandom.current(),会产生多个 ThreadLocalRandom 实例吗?

不会的,见源码:

 1 /** The common ThreadLocalRandom */
 2 static final ThreadLocalRandom instance = new ThreadLocalRandom();
 3 
 4 /**
 5  * Returns the current thread's {@code ThreadLocalRandom}.
 6  *
 7  * @return the current thread's {@code ThreadLocalRandom}
 8  */
 9 public static ThreadLocalRandom current() {
10     if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
11         localInit();
12     return instance;
13 }

放心大胆地使用。

于是示例代码改动如下:

 1 import java.util.concurrent.ThreadLocalRandom;
 2 
 3 public class ThreadLocalRandomDemo {
 4 
 5     public static void main(String[] args) {
 6         for (int i = 0; i < 10; i++) {
 7             new Player().start();
 8         }
 9     }
10 
11     private static class Player extends Thread {
12         @Override
13         public void run() {
14             System.out.println(getName() + ": " + ThreadLocalRandom.current().nextInt(100));
15         }
16     }
17 }

运行一下,可以得到想要的结果:

 1 Thread-0: 90
 2 Thread-3: 77
 3 Thread-2: 97
 4 Thread-5: 96
 5 Thread-4: 42
 6 Thread-1: 3
 7 Thread-6: 4
 8 Thread-7: 6
 9 Thread-8: 52
10 Thread-9: 39

总结一下,在多线程下使用 ThreadLocalRandom 产生随机数时,直接使用 ThreadLocalRandom.current().xxxx

还有一点需要强调的是,ThreadLocalRandom在高并发下的性能远远超过Random,作为服务端开发,这个性能需要考虑,下面我们做一个测试,代码如下:

 1 public class RandomTest {
 2     private static Random random = new Random();
 3 
 4     private static final int N = 100000;
 5 //    Random from java.util.concurrent.
 6     private static class TLRandom implements Runnable {
 7         @Override
 8         public void run() {
 9             double x = 0;
10             for (int i = 0; i < N; i++) {
11                 x += ThreadLocalRandom.current().nextDouble();
12             }
13         }
14     }
15 
16 //    Random from java.util
17     private static class URandom implements Runnable {
18         @Override
19         public void run() {
20             double x = 0;
21             for (int i = 0; i < N; i++) {
22                 x += random.nextDouble();
23             }
24         }
25     }
26 
27     public static void main(String[] args) {
28         System.out.println("threadNum,Random,ThreadLocalRandom");
29         for (int threadNum = 50; threadNum <= 2000; threadNum += 50) {
30             ExecutorService poolR = Executors.newFixedThreadPool(threadNum);
31             long RStartTime = System.currentTimeMillis();
32             for (int i = 0; i < threadNum; i++) {
33                 poolR.execute(new URandom());
34             }
35             try {
36                 poolR.shutdown();
37                 poolR.awaitTermination(100, TimeUnit.SECONDS);
38             } catch (InterruptedException e) {
39                 e.printStackTrace();
40             }
41             String str = "" + threadNum +"," + (System.currentTimeMillis() - RStartTime)+",";
42 
43             ExecutorService poolTLR = Executors.newFixedThreadPool(threadNum);
44             long TLRStartTime = System.currentTimeMillis();
45             for (int i = 0; i < threadNum; i++) {
46                 poolTLR.execute(new TLRandom());
47             }
48             try {
49                 poolTLR.shutdown();
50                 poolTLR.awaitTermination(100, TimeUnit.SECONDS);
51             } catch (InterruptedException e) {
52                 e.printStackTrace();
53             }
54             System.out.println(str + (System.currentTimeMillis() - TLRStartTime));
55         }
56     }
57 }

输出结果如下:

 1 ThreadNum,Random,ThreadLocalRandom 
 2 50,1192,575
 3 100,4031,162
 4 150,6068,223
 5 200,8093,287
 6 250,10049,248
 7 300,12346,200
 8 350,14429,212
 9 400,16491,62
10 450,18475,96
11 500,11311,97
12 550,12421,90
13 600,13577,102
14 650,14718,111
15 700,15896,127
16 750,17101,129
17 800,17907,203
18 850,19261,226
19 900,21576,151
20 950,22206,147
21 1000,23418,174

起始ThreadLocalRandom是对每个线程都设置了单独的随机数种子,这样就不会发生多线程同时更新一个数时产生的资源争抢了,用空间换时间。