Java 隨機數探祕

1 前言

一提到 Java 中的隨機數,不少人就會想到 Random,當出現生成隨機數這樣需求時,大多數人都會選擇使用 Random 來生成隨機數。Random 類是線程安全的,但其內部使用 CAS 來保證線程安全性,在多線程併發的狀況下的時候它的表現是存在優化空間的。在 JDK1.7 以後,Java 提供了更好的解決方案 ThreadLocalRandom,接下來,咱們一塊兒探討下這幾個隨機數生成器的實現到底有何不一樣。算法

2 Random

Random 這個類是 JDK 提供的用來生成隨機數的一個類,這個類並非真正的隨機,而是僞隨機,僞隨機的意思是生成的隨機數實際上是有必定規律的,而這個規律出現的週期隨着僞隨機算法的優劣而不一樣,通常來講週期比較長,可是能夠預測。經過下面的代碼咱們能夠對 Random 進行簡單的使用: 安全

img

Random原理

Random 中的方法比較多,這裏就針對比較常見的 nextInt() 和 nextInt(int bound) 方法進行分析,前者會計算出 int 範圍內的隨機數,後者若是咱們傳入 10,那麼他會求出 [0,10) 之間的 int 類型的隨機數,左閉右開。咱們首先看一下 Random() 的構造方法: bash

img

能夠發如今構造方法當中,根據當前時間的種子生成了一個 AtomicLong 類型的 seed,這也是咱們後續的關鍵所在。微信

nextInt()

nextInt() 的代碼以下所示:多線程

img

這個裏面直接調用的是 next() 方法,傳入的 32,代指的是 Int 類型的位數。併發

img

這裏會根據 seed 當前的值,經過必定的規則(僞隨機算法)算出下一個 seed,而後進行 CAS,若是 CAS 失敗則繼續循環上面的操做。最後根據咱們須要的 bit 位數來進行返回。核心即是 CAS 算法。負載均衡

nextInt(int bound)

nextInt(int bound) 的代碼以下所示:dom

img

這個流程比 nextInt() 多了幾步,具體步驟以下:ide

  1. 首先獲取 31 位的隨機數,注意這裏是 31 位,和上面 32 位不一樣,由於在 nextInt() 方法中能夠獲取到隨機數多是負數,而 nextInt(int bound) 規定只能獲取到 [0,bound) 以前的隨機數,也就意味着必須是正數,預留一位符號位,因此只獲取了31位。(不要想着使用取絕對值這樣操做,會致使性能降低)
  2. 而後進行取 bound 操做。
  3. 若是 bound 是2的冪次方,能夠直接將第一步獲取的值乘以 bound 而後右移31位,解釋一下:若是 bound 是4,那麼乘以4其實就是左移2位,其實就是變成了33位,再右移31位的話,就又會變成2位,最後,2位 int 的範圍其實就是 [0,4) 了。
  4. 若是不是 2 的冪,經過模運算進行處理。

併發瓶頸

在我以前的文章中就有相關的介紹,通常而言,CAS 相比加鎖有必定的優點,但並不必定意味着高效。一個馬上被想到的解決方案是每次使用 Random 時都去 new 一個新的線程私有化的 Random 對象,或者使用 ThreadLocal 來維護線程私有化對象,但除此以外還存在更高效的方案,下面便來介紹本文的主角 ThreadLocalRandom。性能

3 ThreadLocalRandom

在 JDK1.7 以後提供了新的類 ThreadLocalRandom 用來在併發場景下代替 Random。使用方法比較簡單:

ThreadLocalRandom.current().nextInt();
ThreadLocalRandom.current().nextInt(10);
複製代碼

在 current 方法中有:

img
能夠看見若是沒有初始化會對其進行初始化,而這裏咱們的 seed 再也不是一個全局變量,在咱們的Thread中有三個變量:
img

  • threadLocalRandomSeed:ThreadLocalRandom 使用它來控制隨機數種子。
  • threadLocalRandomProbe:ThreadLocalRandom 使用它來控制初始化。
  • threadLocalRandomSecondarySeed:二級種子。

能夠看見全部的變量都加了 @sun.misc.Contended 這個註解,用來處理僞共享問題。

在 nextInt() 方法當中代碼以下:

img

咱們的關鍵代碼以下:

UNSAFE.putLong(t = Thread.currentThread(), SEED,r=UNSAFE.getLong(t, SEED) + GAMMA);
複製代碼

能夠看見因爲咱們每一個線程各自都維護了種子,這個時候並不須要 CAS,直接進行 put,在這裏利用線程之間隔離,減小了併發衝突;相比較 ThreadLocal<Random>,ThreadLocalRandom 不只僅減小了對象維護的成本,其內部實現也更輕量級。因此 ThreadLocalRandom 性能很高。

4 性能測試

除了文章中詳細介紹的 Random,ThreadLocalRandom,我還將 netty4 實現的 ThreadLocalRandom,以及 ThreadLocal<Random> 做爲參考對象,一塊兒參與 JMH 測評。

@BenchmarkMode({Mode.AverageTime})
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 3, time = 5)
@Measurement(iterations = 3, time = 5)
@Threads(50)
@Fork(1)
@State(Scope.Benchmark)
public class RandomBenchmark {

    Random random = new Random();

    ThreadLocal<Random> threadLocalRandomHolder = ThreadLocal.withInitial(Random::new);

    @Benchmark
    public int random() {
        return random.nextInt();
    }

    @Benchmark
    public int threadLocalRandom() {
        return ThreadLocalRandom.current().nextInt();
    }

    @Benchmark
    public int threadLocalRandomHolder() {
        return threadLocalRandomHolder.get().nextInt();
    }

    @Benchmark
    public int nettyThreadLocalRandom() {
        return io.netty.util.internal.ThreadLocalRandom.current().nextInt();
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(RandomBenchmark.class.getSimpleName())
                .build();

        new Runner(opt).run();
    }

}
複製代碼

測評結果以下:

Benchmark                                Mode  Cnt     Score     Error  Units
RandomBenchmark.nettyThreadLocalRandom   avgt    3   192.202 ± 295.897  ns/op
RandomBenchmark.random                   avgt    3  3197.620 ± 380.981  ns/op
RandomBenchmark.threadLocalRandom        avgt    3    90.731 ±  39.098  ns/op
RandomBenchmark.threadLocalRandomHolder  avgt    3   229.502 ± 267.144  ns/op
複製代碼

從上圖能夠發現,JDK1.7 的 ThreadLocalRandom 取得了最好的成績,僅僅須要 90 ns 就能夠生成一次隨機數,netty 實現的ThreadLocalRandom 以及使用 ThreadLocal 維護 Random 的方式差距不是很大,位列 二、3 位,共享的 Random 變量則效果最差。

可見,在併發場景下,ThreadLocalRandom 能夠明顯的提高性能。

5 注意點

注意,ThreadLocalRandom 切記不要調用 current 方法以後,做爲共享變量使用

public class WrongCase {
    
    ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
    
    public int concurrentNextInt(){
        return threadLocalRandom.nextInt();
    }
    
}
複製代碼

這是由於 ThreadLocalRandom.current() 會使用初始化它的線程來填充隨機種子,這會帶來致使多個線程使用相同的 seed。

public class Main {

    public static void main(String[] args) {
        ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
        for(int i=0;i<10;i++)
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(threadLocalRandom.nextInt());
            }
        }).start();

    }
}
複製代碼

輸出相同的隨機數:

-1667209487
-1667209487
-1667209487
-1667209487
-1667209487
-1667209487
-1667209487
-1667209487
-1667209487
-1667209487
複製代碼

請在確保不一樣線程獲取不一樣的 seed,最簡單的方式即是每次調用都是使用 current():

public class RightCase {
    public int concurrentNextInt(){
        return ThreadLocalRandom.current().nextInt();
    }
}
複製代碼

彩蛋1

梁飛博客中一句話經常在我腦海中縈繞:魔鬼在細節中。優秀的代碼都是一個個小細節堆砌出來,今天介紹的 ThreadLocalRandom 也不例外。

dubbo

在 incubator-dubbo-2.7.0 中,隨機負載均衡器的一個小改動即是將 Random 替換爲了 ThreadLocalRandom,用於優化併發性能。

彩蛋2

ThreadLocalRandom 的 nextInt(int bound) 方法中,當 bound 不爲 2 的冪次方時,使用了一個循環來修改 r 的值,我認爲這可能沒必要要,你以爲呢?

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;
}
複製代碼

歡迎關注個人微信公衆號:「Kirito的技術分享」,關於文章的任何疑問都會獲得回覆,帶來更多 Java 相關的技術分享。

關注微信公衆號
相關文章
相關標籤/搜索