一文搞懂Java隨機數生成

JavaDog發表於2019-01-30

你是如何生成隨機資料的?

是這樣?

new Random().nextInt();
複製程式碼

是這樣?

org.apache.commons.lang3.RandomUtils.nextInt(startInclusive, endExclusive);
複製程式碼

還是這樣?

ThreadLocalRandom.current().nextInt();
複製程式碼

先說結論

  • 併發場景下隨機數生成優先請用**ThreadLocalRandom**
  • 少併發場景可用org.apache.commons.lang3.RandomUtils
  • 安全隨機數場景請使用java.security.**SecureRandom**,推薦演算法**SHA1PRNG**(PRNG->pseudo-random numer generator )
  • 不建議直接使用new Random().nextInt()

Random生成的隨機數真的“隨機”嗎?

我們可以在java.util.Random類註釋中找到答案:

An instance of this class is used to generate a stream of pseudorandom numbers. The class uses a 48-bit seed, which is modified using a linear congruential formula. (See Donald Knuth, , Section 3.2.1.)

If two instances of Random are created with the same seed, and the same sequence of method calls is made for each, they will generate and return identical sequences of numbers.

譯文:

該類的例項用於生成 偽隨機數。該類使用48位種子,使用線性同餘公式進行修改。 (參見Donald Knuth,<計算機程式設計的藝術,第2卷>,第3.2.1節。)

如果使用相同的種子建立兩個Random例項,並且對每個例項都進行相同的方法呼叫,則它們將生成相同的隨機數字。

所以可以看到其實Random生成的隨機數都是偽隨機數,只要種子一樣,那麼生成的隨機數是完全一樣的,可以看到seed對隨機數生成至關重要。

Random原理淺析

直接上碼, Random建構函式

    private static final AtomicLong seedUniquifier  = new AtomicLong(8682522807148012L);

    public Random() {
        //獲取一個seed和當前nanoTime異或後,呼叫有參建構函式構建例項
        this(seedUniquifier() ^ System.nanoTime());
    }

    private static long seedUniquifier() {
        // L'Ecuyer, "Tables of Linear Congruential Generators of
        // Different Sizes and Good Lattice Structure", 1999

        for (;;) {
            //獲取當前seed的初始值,這個值是Random類的靜態變數,用來保證每次構造Random例項的初始seed是不一樣的,增強seed的差異
            long current = seedUniquifier.get();

            //為什麼要乘以這數值,應該要看看線性同餘公式了
            long next = current * 181783497276652981L;

            //把最新的值CAS更新到seedUniquifier
            if (seedUniquifier.compareAndSet(current, next))
                return next;
        }
    }

    public Random(long seed) {
        if (getClass() == Random.class)
            this.seed = new AtomicLong(initialScramble(seed));
        else {
            // subclass might have overriden setSeed
            this.seed = new AtomicLong();
            setSeed(seed);
        }
    }
複製程式碼

可以看到Random的建構函式,就做了一件事情,就是為Random例項構建了一個初始的seed。

再看nextInt方法

    public int nextInt(int bound) {
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);
        //根據上一個seed生成一個新的seed
        int r = next(31);
        int m = bound - 1;
        //下面是演算法,根據新的seed計算最終的隨機數
        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;
    }

    protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
            oldseed = seed.get();
            //根據當前seed值計算新的seed
            nextseed = (oldseed * multiplier + addend) & mask;
            //使用nextSeed通過CAS+自旋的方式更新seed的值
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
    }

複製程式碼

通過上面的程式碼可知隨機數的生成主要是兩個核心步驟:

  • 首先需要根據oldSeed計算生成nextSeed。
  • 然後根據nextSeed和bound變數通過一定的演算法來計算生成新的隨機數。

Random的侷限

在併發場景下使用單個Random例項生成隨機數時候,多個執行緒同時呼叫next(int bits)計算nextSeed時候,多個執行緒會競爭同一個seed的更新操作,但是由於seed的更新是CAS+自旋的方式,同一時間只有一個執行緒會成功,所以 Random例項是執行緒安全 的。但是CAS操作在大併發場景下會有大量執行緒更新失敗,然後進行自旋重試,直至成功,而大量執行緒的自旋重試是會降低併發效能和消耗CPU資源的,為了解決這個問題,ThreadLocalRandom類應運而生。

ThreadLocalRandom原理淺析

ThreadLocalRandom是怎麼解決併發場景下因自旋重試導致的效能下降呢?
核心思路是 把seed的值從Random的成員變數轉移到了Thread裡面的成員變數 ,從而達到在併發場景下 去鎖 的目的,進而實現了併發效能的大幅提升。

使用方式:

ThreadLocalRandom.current().nextInt();
複製程式碼

先看ThreadLocalRandom.current()

    /** The common ThreadLocalRandom */
    static final ThreadLocalRandom instance = new ThreadLocalRandom();

    /** Constructor used only for static singleton */
    private ThreadLocalRandom() {
        initialized = true; // false during super() call
    }
    //=======================分割線=======================
    public static ThreadLocalRandom current() {
        //噹噹前執行緒的probe等於0時,初始化執行緒的seed欄位
        if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
            localInit();
        //返回單例
        return instance;
    }
    static final void localInit() {
        int p = probeGenerator.addAndGet(PROBE_INCREMENT);
        int probe = (p == 0) ? 1 : p; // skip 0
        //獲取一個seed
        long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
        Thread t = Thread.currentThread();
        //通過UNSAFE把seed設定到thread裡面
        UNSAFE.putLong(t, SEED, seed);
        //通過UNSAFE把probe設定為非0,這樣下一次就不需要重新初始化了
        UNSAFE.putInt(t, PROBE, probe);
    }
    //=======================分割線=======================
    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long SEED;//Thread類的threadLocalRandomSeed欄位的偏移量
    private static final long PROBE;//Thread類的threadLocalRandomProbe欄位的偏移量
    private static final long SECONDARY;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> tk = Thread.class;
            SEED = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSeed"));
            PROBE = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomProbe"));
            SECONDARY = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
複製程式碼

再看 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;
    }
    //獲取當前執行緒的下一個seed值
    final long nextSeed() {
        Thread t; long r; // read and update per-thread seed
        //先把當前執行緒的seed獲取出來,然後+GAMMA(相當於一個步長)再塞回去
        UNSAFE.putLong(t = Thread.currentThread(), SEED,
                       r = UNSAFE.getLong(t, SEED) + GAMMA);
        return r;
    }

複製程式碼

再來一張圖來幫助下理解:
random.png

SecureRandom為什麼是安全的

SecureRandom和Random都是,也是如果種子一樣,產生的隨機數也一樣: 因為種子確定,隨機數演算法也確定,因此輸出是確定的。

只是說,SecureRandom類收集了一些 隨機事件 ,比如從IO中斷,網路卡傳輸包等等這些外部入侵者 不可預測 的隨機源中獲取熵,SecureRandom 使用這些隨機事件作為種子。這意味著,種子是不可預測的,而不像Random預設使用系統當前時間的毫秒數作為種子,有規律可尋。

至於為什麼選用 SHA1PRNG 的效能更優,詳見:《SecureRandom的江湖偏方與真實效果》calvin1978.blogcn.com/articles/se…

效能比較

使用JMH測試

# JMH version: 1.21
# VM version: JDK 1.8.0_151, Java HotSpot(TM) 64-Bit Server VM, 25.151-b12
# Warmup: 3 iterations, 10 s each
# Measurement: 3 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 5 threads, will synchronize iterations
# Benchmark mode: Throughput, ops/time
Benchmark                  Mode  Cnt          Score           Error  Units
new Random().nextInt      thrpt    3   19614207.388 ±   1164741.166  ops/s
RandomUtils.nextInt       thrpt    3   18046679.473 ±   3182634.593  ops/s
ThreadLocalRandom.nextInt thrpt    3  845131847.300 ± 185973223.877  ops/s
SecureRandom.nextInt      thrpt    3   28169402.475 ±   2356713.939  ops/s
複製程式碼

碼字不易,如有建議請掃碼一文搞懂Java隨機數生成


相關文章