Random和ThreadLocalRandom


在日常項目開發中,隨機的場景需求經常發生,如紅包、負載均衡等等。在Java中的,使用隨機,一般使用Random或者Math.random()。這篇文章中主要就來介紹下Random,以及在並發環境下一些更好的選擇ThreadLocalRandom。

一.Random

1.Random使用

Random類位於java.util包下,是一種偽隨機。它主要提供了一下幾種不同類型的隨機數接口:

  • nextBoolean(),boolean類型的隨機,隨機返回true/false
  • nextFloat(),float類型的隨機,隨機返回0.0 - 1.0之間的float類型
  • nextDouble(),double類型的隨機,隨機返回0.0 - 1.0之間的double類型
  • nextLong(),long類型的隨機,隨機返回long類型的隨機數
  • nextInt(),int類型的隨機,隨機返回int類型的隨機數
  • nextInt(int bound),同nextInt(),但是返回的int上界是bound,且不包括bound,下界是0
  • nextGaussian(),double類型的隨機,隨機返回0.0 - 1.0之前的double類型,但是它整體會表現出高斯分布

Random中,十分關鍵的是Seed,它是一個48bit的的隨機種子。換而言之,Random的隨機是一種偽隨機,它也是一套非常復雜的算法,生成的隨機數也是有規律可循。這套算法的執行需要一個初始數值,在Random中初始值,就是seed隨機種子。對於相同的Seed,調用隨機方法相同,得到的隨機數是一樣的。

接下來,看一個使用Random實現隨機紅包的功能。輸入兩個參數,第一個是紅包總金額,第二個是紅包個數:

    /**
     * 隨機紅包實現
     *
     * @param totalAmount 紅包總金額
     * @param nums 紅包個數
     * @author huaijin
     */
    static List<Long> randomRedEnvelope(Long totalAmount, int nums) {
        if (nums == 0) {
            throw new RuntimeException("紅包個數需要大於0.");
        }
        List<Long> redEnvelope = new ArrayList<>(nums);
        Random random = new Random();
        long remaining = totalAmount;
        for (int i = 0; i < nums - 1; i++) {
            double probability = random.nextDouble();
            Long subAmount = Math.round(remaining * probability);
            if (subAmount == 0 || remaining - subAmount == 0) {
                continue;
            }
            redEnvelope.add(subAmount);
            remaining = remaining - subAmount;
        }
        redEnvelope.add(remaining);
        return redEnvelope;
    }

這里利用Random的nextDouble()的特性,每次生成0.0 - 1.0之間的數字,作為紅包的占比,以達到隨機紅包的實現。

2.Random線程安全保證

Random在多線程的環境是並發安全的,它解決競爭的方式是使用用原子類,本質上上也就是CAS + Volatile保證線程安全。接下來就分析下其原理,以理解線程安全的實現。

在Random類中,有一個AtomicLong的域,用來保存隨機種子。其中每次生成隨機數時都會根據隨機種子做移位操作以得到隨機數。如Long類型的隨機:

long類型在Java中總弄64bit,對next方法的返回值左移32作為long的高位,然后將next方法返回值作為低32位,作為long類型的隨機數。此處關鍵之處在於next方法,以下是next方法的核心

使用seed種子,不斷生成新的種子,然后使用CAS將其更新,再返回種子的移位后值。這里不斷的循環CAS操作種子,直到成功。

可見,Random實現原理主要是利用隨機種子采用一定算法進行處理生成隨機數,在隨機種子的安全保證利用原子類AtomicLong。

3.並發下Random的不足

以上分析了Random的實現原理,雖然Random是線程安全,但是對於並發處理使用原子類AtomicLong在大量競爭時,由於很多CAS操作會造成失敗,不斷的Spin,而造成CPU開銷比較大而且吞吐量也會下降。

現在發現問題就是大量的並發競爭,使得CAS失敗,對於競爭問題的優化策略在前文AtomicLong和LongAdder時,也談到了。鎖的極限優化是Free Lock,如ThreadLoal方式。

在JDK 1.7中由並發大神引入了ThreadLocalRandom來解決Random的大並發問題,以下兩者的測試結果比較。每個線程生成10w次,運行12次,去掉了最大最小值的平均結果:

可以看出Random隨着競爭越來越激烈,然后耗時越來越多,說明吞吐量將成倍下降。然而ThreadLocalRandom隨着線程數的增加,基本沒有變化。所以在大並發的情況下,隨機的選擇,可以考慮ThreadLocalRandom提升性能,也是性能優化之道的一步。

二.更好的選擇ThreadLocalRandom

ThreadLocalRandom是Random的子類,它是將Seed隨機種子隔離到當前線程的隨機數生成器,從而解決了Random在Seed上競爭的問題,它的處理思想和ThreadLocal本質相同。這里開門見山,直接看源碼,分析其實現原理。

使用ThreadLocalRandom的方式為

ThreadLocalRandom.current().nextX(...)

其中X表示,Int、Long、Double、Float、Boolean等等。按照這樣的方法調用逐步深入其中細節:

從上述代碼中顯而意見就可以看出使用了單例模式,當UNSAFE.getInt(Thread.currentThread(), PROBE)返回0時,就執行localInit(),否則就返回單例。

首先看單例instance

從注釋中也可以看出,是一個公共的ThreadLocalRandom,也就是說,在一個Java應用中只有一個ThreadLocalRandom對象,顯然是單例,即無論哪個線程執行隨機時都是使用這個單例對象。

那么單例情況下又如何將隨機種子隔離呢?

再來看下UNSAFE.getInt(Thread.currentThread(), PROBE),這條語句主要是獲取當前Thread對象中的PROBE,再看看PROBE的初始化

PROBE是Thread中threadLocalRandomProbe

從注釋中可以看出,threadLocalRandomProbe用於表示ThreadLocalRandom是否初始化,如果是非0,表示其已經初始化。換句話說,該變量就是狀態變量,用於標識ThreadLocalRandom是否被初始化。

其中還有個非常關鍵的threadLocalRandomSeed,從注釋中也可以看出,它是當前線程的隨機種子。到這里,一下子豁然開朗,隨機種子分散在各個Thread對象中,從而避免了並發時的競爭點。

那么它又是什么時候初始化的呢?

當Thread對象被創建后,threadLocalRandomProbe和threadLocalRandomSeed應該都是0。當在這個線程中首次調用ThreadLocalRandom.current時,threadLocalRandomProbe為0,會執行localInit。其中會初始化threadLocalRandomSeed,並將threadLocalRandomProbe更新為非0,表示已經初始化。

上面核心的兩步驟,初始化Thread中的threadLocalRandomSeed和threadLocalRandomProbe。

當localInit執行后,就返回ThreadLocalRandom的單例供應用使用nextX()系列方法生成隨機數。再來看下nextInt的實現

從以上可以看出,當生成int隨機數時,每次都利用Unsafe工具獲取當前Thread對象中的隨機種子生成隨機數。並且每次獲取的時候,都將Seed種子增加GAMMA,以供下次使用。

三.總結

Random是Java中提供的隨機數生成器工具類,但是在大並發的情況下由於其隨機種子的競爭會導致吞吐量下降,從而引入ThreadLocalRandom。它將競爭點隔離到每個線程中,從而消除了大並發情況下競爭問題,提升了性能。

從兩者的設計上,可以看出在處理並發優化時的優秀設計思想,對於競爭問題,可以將將競爭點隔離,如使用ThreadLocal實現。

並發競爭的整體優化思路,還是像前文中總結的一樣:

lock -> cas + volatile -> free lock

只會free lock的設計方式就是避免競爭,將競爭點隔離到線程中,從而解決競爭。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM