《 面試又翻車了》此次居然和 Random 有關?

小強最近面試又翻車了,然而令他鬱悶的是,此次居然是栽到了本身常常在用的 Random 上......
面試問題java

既然已經有了 Random 爲何還須要 ThreadLocalRandom?
正文git

Random 是使用最普遍的隨機數生成工具了,即便連 Math.random() 的底層也是用 Random 實現的Math.random() 源碼以下:
《 面試又翻車了》此次居然和 Random 有關?
能夠看出 Math.random() 直接指向了 Random.nextDouble() 方法。
Random 使用github

這開始以前,咱們先來了解一下 Random 的使用。面試

Random random = new Random();
for (int i = 0; i < 3; i++) {
    // 生成 0-9 的隨機整數
    random.nextInt(10);
}

以上程序的執行結果爲:編程

1
0
7

Random 源碼解析多線程

能夠看出 Random 是經過 nextInt() 方法生成隨機整數的,那他的底層的是如何實現的呢?咱們來看他的實現源碼:併發

/**
 * 源碼版本:JDK 11
 */
public int nextInt(int bound) {
    // 驗證邊界的合法性
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);
    // 根據老種子生成新種子
    int r = next(31);
    // 計算最大值
    int m = bound - 1;
    // 根據新種子計算隨機數
    if ((bound & m) == 0)  // i.e., bound is a power of 2
        r = (int)((bound * (long)r) >> 31);
    else {
        for (int u = r;
                u - (r = u % bound) + m < 0;
                u = next(31))
            ;
    }
    return r;
}

從以上源碼咱們能夠看出,整個源碼最核心的部分有兩塊:
根據老種子生成新種子;
根據新種子計算出隨機數。
根據新種子計算出隨機數的代碼已經很明確了,咱們須要確認一下 next() 方法是如何實現的,繼續看源碼:dom

/**
 * 源碼版本:JDK 11
 */
protected int next(int bits) {
    // 聲明老種子和新種子
    long oldseed, nextseed;
    AtomicLong seed = this.seed;
    do {
        // 獲取原子變量種子的值
        oldseed = seed.get();
        // 根據當前種子計算出新種子的值
        nextseed = (oldseed * multiplier + addend) & mask;
    } while (!seed.compareAndSet(oldseed, nextseed)); // 使用 CAS 更新種子
    return (int)(nextseed >>> (48 - bits));
}

根據以上源碼能夠看出,在使用老種子去獲取新種子的時候,若是是多線程操做,則同一時刻只會有一個線程 CAS (Conmpare And Swap,比較並交換) 成功,其餘失敗的線程會經過自旋等待獲取新種子,所以會有必定的性能消耗。
當多線程使用同一個老種子來 CAS 的時候,只能有一個線程可以成功,而其餘失敗的線程只能經過自旋等待,這也是爲何 JDK 1.7 會引入 ThreadLocalRandom 的答案了,它主要爲了提高多線程狀況下 Random 的執行效率。
ThreadLocalRandom 使用ide

咱們先來看 ThreadLocalRandom 的類關係圖:
《 面試又翻車了》此次居然和 Random 有關?
能夠看出 ThreadLocalRandom 繼承於 Random 類,先來看它的使用:高併發

ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
for (int i = 0; i < 3; i++) {
    // 生成 0-9 的隨機數
    System.out.println(threadLocalRandom.nextInt(10));
}

以上程序的執行結果爲:

1
7
5

能夠看出 ThreadLocalRandom 和 Random 同樣,都是經過 nextInt() 方法實現隨機整數生成的。
ThreadLocalRandom 源碼解析

接下來咱們來看 ThreadLocalRandom 的隨機數是如何生成的,源碼以下:

/**
 * 源碼版本:JDK 11
 */
public int nextInt(int bound) {
    if (bound <= 0)
        throw new IllegalArgumentException(BAD_BOUND);
    // 根據老種子生成新種子
    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;
}

從以上源碼能夠看出 ThreadLocalRandom 的 nextInt() 和 Random 的 nextInt() 在寫法和實現思路都很像,他們主要的區別在 nextSeed() 方法上,源碼以下:

/**
 * 源碼版本:JDK 11
 */
final long nextSeed() {
    Thread t; long r; // read and update per-thread seed
    // 把當前線程做爲參數生成一個新種子
    U.putLong(t = Thread.currentThread(), SEED,
              r = U.getLong(t, SEED) + GAMMA);
    return r;
}

@HotSpotIntrinsicCandidate
public native void putLong(Object o, long offset, long x);
從以上源碼能夠看出,ThreadLocalRandom 並非像 Thread 那樣使用 CAS 和自旋來獲取新種子,而是在每一個線程中使用每一個線程中保存本身的老種子來生成新種子,所以就能夠避免多線程競爭和自旋等待的時間,因此在多線程環境下性能更高。
ThreadLocalRandom 注意事項

在使用 ThreadLocalRandom 時須要注意一下,在多線程不能共享一個 ThreadLocalRandom 對象,不然會形成生成的隨機數都相同,以下代碼所示:

// 聲明多線程
ExecutorService service = Executors.newCachedThreadPool();
// 共享 ThreadLocalRandom
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
for (int i = 0; i < 10; i++) {
    // 多線程執行隨機數並打印結果
    service.submit(() -> {
        System.out.println(Thread.currentThread().getName() + ":" + threadLocalRandom.nextInt(10));
        ;
    });
}

以上程序執行結果以下:

pool-1-thread-2:4
pool-1-thread-1:4
pool-1-thread-3:4
pool-1-thread-10:4
pool-1-thread-6:4
pool-1-thread-7:4
pool-1-thread-4:4
pool-1-thread-9:4
pool-1-thread-8:4
pool-1-thread-5:4

Random VS ThreadLocalRandom

Random 生成獲取新種子,以下圖所示:
《 面試又翻車了》此次居然和 Random 有關?
ThreadLocalRandom 生成獲取新種子,以下圖所示:
《 面試又翻車了》此次居然和 Random 有關?

性能對比

接下來咱們使用 Oracle 官方提供的性能測試工具 JMH (Java Microbenchmark Harness,JAVA 微基準測試套件),來測試一下 Random 和 ThreadLocalRandom 的吞吐量(單位時間內成功執行程序的數量):

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

/**
 * JDK:11
 * Windows 10 I5-4460/16G
 */
@BenchmarkMode(Mode.Throughput) // 測試類型:吞吐量
//@Threads(16)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class RandomExample {
    public static void main(String[] args) throws RunnerException {
        // 啓動基準測試
        Options opt = new OptionsBuilder()
                .include(RandomExample.class.getSimpleName()) // 要導入的測試類
                .warmupIterations(5) // 預熱 5 輪
                .measurementIterations(10) // 度量10輪
                .forks(1)
                .build();
        new Runner(opt).run(); // 執行測試
    }

    /**
     * Random 性能測試
     */
    @Benchmark
    public void randomTest() {
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            // 生成 0-9 的隨機數
            random.nextInt(10);
        }
    }
    /**
     * ThreadLocalRandom 性能測試
     */
    @Benchmark
    public void threadLocalRandomTest() {
        ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
        for (int i = 0; i < 10; i++) {
            threadLocalRandom.nextInt(10);
        }
    }
}

測試結果以下:
《 面試又翻車了》此次居然和 Random 有關?
其中,Cnt 表示運行了多少次,Score 表示執行的成績,Units 表示每秒的吞吐量。
從 JMH 測試的結果能夠看出,ThreadLocalRandom 在併發狀況下的吞吐量約是 Random 的 5 倍。
完整基準測試代碼下載:https://github.com/vipstone/blog-example/blob/master/blog-example/src/main/java/com/example/RandomExample.java
總結

本文講了 Random 和 ThreadLocalRandom 的使用以及源碼分析,Random 是經過 CAS 和自旋的方式生成隨機數,在多線程模式下同一時刻只能有一個線程經過 CAS 獲取到新種子並生成隨機數,其餘線程只能自旋等待,因此有必定的性能損耗。而在 JDK 1.7 時新增了 ThreadLocalRandom 它的種子保存在各自的線程中,所以不會有自旋等待的過程,因此高併發狀況下性能更優秀。
最後,咱們經過官方提供的基準測試工具 JMH 獲得的結果,ThreadLocalRandom 的性能大約是 Random 的 5 倍,因此在高併發狀況下儘可能使用 ThreadLocalRandom。
參考 & 鳴謝 《Java 併發編程之美》翟陸續
原創不易,期待你的素質三連,ღ( ´・ᴗ・` )比心~
【END】
近期熱文

由於我說:volatile 是輕量級的 synchronized,面試官讓我回去等通知!
有人說:輕量級鎖必定比重量級鎖快!我忍不住笑了
如何模擬線程池溢出?線程池溢出後會怎樣?「視頻版」
關注下方二維碼,訂閱更多精彩內容
《 面試又翻車了》此次居然和 Random 有關?

相關文章
相關標籤/搜索