多線程下ThreadLocalRandom用法


前言

學習 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是對每個線程都設置了單獨的隨機數種子,這樣就不會發生多線程同時更新一個數時產生的資源爭搶了,用空間換時間。 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM