天天看點

多線程下ThreadLocalRandom用法

學習 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      

繼續閱讀