1. 程式人生 > >ThreadLocalRandom ---- Random在大併發環境下的替代者

ThreadLocalRandom ---- Random在大併發環境下的替代者


本部落格系列是學習併發程式設計過程中的記錄總結。由於文章比較多,寫的時間也比較散,所以我整理了個目錄貼(傳送門),方便查閱。

併發程式設計系列部落格傳送門


隨機數

隨機數在科學研究與工程實際中有著極其重要的應用!

簡單來說,隨機數就是一個數列,這個數列可能滿足一定的概率分佈,又獲取其滿足的分佈並不為我們所知。

數學方法產生隨機數應該稱之為“偽隨機數”,只有使用物理方法才能得到真正的隨機數!因此我們使用計算機產生的隨機數都是"偽隨機數"。那麼計算機到底是怎麼產生隨機數的呢?這時就要提到隨機數發生器了。

隨機數發生器

我們高中的時候都學過數列的知識,上面提到隨機數可以看成是一個數列,那麼我們可以將隨機數發生器看成是一個數列表達式。比如現在有下面兩個隨機說發生器

//發生器1
X(n+1)= a * X(n) + b

//發生器2
X(n+1)= a * X(n)

當然還有很多隨機數發生器,現實生產中使用的發生器也並不是像上面的那麼簡單,這邊只是為了說明隨機數發生器到底是什麼列了兩個例子。

隨機數種子

我們在產生隨機數的時候經常會聽到隨機數種子這個名詞,那隨機數種子到底是什麼?我們還是以上面的發生器為例。

//發生器1
X(n+1)= a * X(n) + b

顯然通過上式我們能夠得到一個數列,前提是X(0)應該給出,依次我們就可以算出X(1),X(2)...;當然不同的X(0)就會得到不同的數列。

可以說X(0)的值就是隨機數的種子,只要這個種子給的一樣,產生的隨機數序列就是一樣的。下面給出一個使用Java中￿￿Random

產生隨機數的列子證明下這個說法。

Random random1 = new Random(100);
for (int i = 0; i < 10 ; i++) {
    System.out.println(random1.nextInt(5));
}

System.out.println("-------------");

Random random2 = new Random(100);
for (int i = 0; i < 10 ; i++) {
    System.out.println(random2.nextInt(5));
}

執行結果如下:

0
0
4
3
1
1
1
3
3
3
-------------
0
0
4
3
1
1
1
3
3
3
--------------

上面程式碼中新建了兩個隨機數發生器,都設定了同樣的隨機數種子100,產生10個隨機數。從上面的結果中可以看出兩個發生器產生的序列是一樣的。

對於一個應用級的偽隨機數發生器,所有的“偽隨機數”,均勻的分佈於一個“軌道”上,幾乎所有的數都可以做為種子。數字“0”,有時是一個特例,不能作為種子,當然它取決於你使用的隨機數發生器!

Random類的侷限性

Random類是JDK提供的一個隨機數發生器。 我們看下Random類中nextInt方法的原始碼:


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方法到底是怎樣生成新的隨機數種子的。


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));
    return (int)(nextseed >>> (48 - bits));
}

上面程式碼中,首先獲取當前原子變數種子的值,然後根據當前種子值計算新的種子。再然後使用CAS機制更新種子的值,保證多執行緒競爭的情況下只有一個能更新成功。最後使用固定演算法根據新的種子計算隨機數。

每個Random例項裡面都有一個原子性的種子變數用來記錄當前的種子值,當要生成新的隨機數時需要根據當前種子計算新的種子並更新回原子變數。在多執行緒下使用單個Random例項生成隨機數時,當多個執行緒同時計算隨機數來計算新的種子時,多個執行緒會競爭同一個原子變數的更新操作,由於原子變數的更新是CAS操作,同時只有一個執行緒會成功,所以會造成大量執行緒進行自旋重試,這會降低併發效能。

分析到這裡我們可以看出Random的侷限性並不是執行緒安全的問題,而是在大量執行緒併發的時候,通過CAS機制更新隨機數種子會導致大量執行緒自旋,耗費CPU效能,導致系統吞吐量下降。

ThreadLocalRandom

ThreadLocalRandom類是JDK 7在JUC包下新增的隨機數生成器,它彌補了Random類在多執行緒下的缺陷。下面來分析下ThreadLocalRandom的實現原理。

從名字上看它會讓我們聯想到ThreadLocal。ThreadLocal通過讓每一個執行緒複製一份變數,使得在每個執行緒對變數進行操作時實際是操作自己本地記憶體裡面的副本,從而避免了對共享變數進行同步。
實際上ThreadLocalRandom的實現也是這個原理,Random的缺點是多個執行緒會使用同一個原子性種子變數,從而導致對原子變數更新的競爭。

那麼,如果每個執行緒都維護一個種子變數,則每個執行緒生成隨機數時都根據自己老的種子計算新的種子,並使用新種子更新老的種子,再根據新種子計算隨機數,就不會存在競爭問題了,這會大大提高併發效能。

ThreadLocalRandom提升效能的原理就是這樣的。具體的原始碼也比較簡單,這邊就不貼程式碼了。感興趣的可以自己看下。下面貼下ThreadLocalRandom的簡單使用方法

ThreadLocalRandom random = ThreadLocalRandom.current();
random.nextInt();

參考

  • 《Java併發程式設計之美》