前言
學習 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
總結一下,在多線程下使用 ThreadLocalRandom
產生隨機數時,直接使用 ThreadLocalRandom.current(100)
參考
https://segmentfault.com/q/1010000010292276
https://blog.csdn.net/zhailuxu/article/details/79073439
來源於: