多線程下 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

總結一下,在多線程下使用 ThreadLocalRandom 產生隨機數時,直接使用 ThreadLocalRandom.current(100)

 

參考

https://segmentfault.com/q/1010000010292276
https://blog.csdn.net/zhailuxu/article/details/79073439

 


來源於:

https://www.jianshu.com/p/89dfe990295c

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章