學習 ThreadLocalRandom 的時候遇到一些疑惑,為何使用它在多線程下會産生相同的随機數?
閱讀源碼後終于稍微了解了一些它的運作機制,總結出它在多線程下正确的用法,特此記錄。
ThreadLocalRandom的用處
在多線程下,使用 java.util.Random 産生的執行個體來産生随機數是線程安全的,但深挖 Random 的實作過程,會發現多個線程會競争同一 seed 而造成性能降低。
其原因在于:
Random 生成新的随機數需要兩步:
- 根據老的 seed 生成新的 seed
- 由新的 seed 計算出新的随機數
其中,第二步的算法是固定的,如果每個線程并發地擷取同樣的 seed,那麼得到的随機數也是一樣的。為了避免這種情況,Random 使用 CAS 操作保證每次隻有一個線程可以擷取并更新 seed,失敗的線程則需要自旋重試。
是以,在多線程下用 Random 不太合适,為了解決這個問題,出現了 ThreadLocalRandom,在多線程下,它為每個線程維護一個 seed 變量,這樣就不用競争了。
但是我在使用的時候,發現 ThreadLocalRandom 在多線程下産生了相同的随機數,這是怎麼回事呢?
ThreadLocalRandom多線程下産生相同随機數
來看一下産生相同随機數的示例代碼:
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));
}
}
}
運作該代碼,結果如下:
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
為此,我閱讀了 ThreadLocalRandom 的源碼,從中找到了端倪。
先是靜态 current() 方法:
public static ThreadLocalRandom current() {
//如果線程第一次調用 current() 方法,執行 localInit()方法
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
初始化方法 localInit()中,為線程初始化了 seed,并儲存在 UNSAFE 裡,這裡 UNSAFE 的方法是 native 方法,我不太了解,但并不影響了解。可以把這裡的操作看作是初始化了 seed,把線程和 seed 以鍵值對的形式儲存起來。
static final void localInit() {
int p = probeGenerator.addAndGet(PROBE_INCREMENT);
int probe = (p == 0) ? 1 : p; // skip 0
long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
Thread t = Thread.currentThread();
UNSAFE.putLong(t, SEED, seed);
UNSAFE.putInt(t, PROBE, probe);
}
當要生成随機數的時候,調用 nextInt() 方法:
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;
}
這裡主要關注 第一處 的 nextSeed() 方法:
final long nextSeed() {
Thread t; long r; // read and update per-thread seed
UNSAFE.putLong(t = Thread.currentThread(), SEED,
r = UNSAFE.getLong(t, SEED) + GAMMA);
return r;
}
好了,問題來了!這裡傳回的值是 r = UNSAFE.getLong(t, SEED) + GAMMA,是從 UNSAFE 裡取出來的。但問題是,這裡取出來的值對不對?或者說,能否取出來?
回到示例代碼,我們在主線程調用了 TreadLocalRandom 的 current() 方法,該方法把主線程和主線程的 seed 存入了 UNSAFE。
接下來,我們在非主線程調用 nextInt(),但非主線程和 seed 的鍵值對之前并沒有存入 UNSAFE 。但我們卻從 UNSAFE 裡取非主線程的 seed 值,雖然我不知道取出來的 seed 到底是什麼,但肯定不是多線程下想要的結果,而這也導緻了多線程下産生的随機數是重複的。
那麼在多線程下如何正确地使用 ThreadLocalRandom 呢?
ThreadLocalRandom多線程下正确用法
結合上述分析,正确地使用 ThreadLocalRandom,肯定需要給每個線程初始化一個 seed,那就需要調用 ThreadLocalRandom.current() 方法。
那麼有個疑問,在每個線程裡都調用 ThreadLocalRandom.current(),會産生多個 ThreadLocalRandom 執行個體嗎?
不會的,見源碼:
/** The common ThreadLocalRandom */
static final ThreadLocalRandom instance = new ThreadLocalRandom();
/**
* Returns the current thread's {@code ThreadLocalRandom}.
*
* @return the current thread's {@code ThreadLocalRandom}
*/
public static ThreadLocalRandom current() {
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
放心大膽地使用。
于是示例代碼改動如下:
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.current().nextInt(100));
}
}
}
Thread-0: 90
Thread-3: 77
Thread-2: 97
Thread-5: 96
Thread-4: 42
Thread-1: 3
Thread-6: 4
Thread-7: 6
Thread-8: 52
Thread-9: 39