本博客系列是學習並發編程過程中的記錄總結。由於文章比較多,寫的時間也比較散,所以我整理了個目錄貼(傳送門),方便查閱。
隨機數
隨機數在科學研究與工程實際中有着極其重要的應用!
簡單來說,隨機數就是一個數列,這個數列可能滿足一定的概率分布,又獲取其滿足的分布並不為我們所知。
數學方法產生隨機數應該稱之為“偽隨機數”,只有使用物理方法才能得到真正的隨機數!因此我們使用計算機產生的隨機數都是"偽隨機數"。那么計算機到底是怎么產生隨機數的呢?這時就要提到隨機數發生器了。
隨機數發生器
我們高中的時候都學過數列的知識,上面提到隨機數可以看成是一個數列,那么我們可以將隨機數發生器看成是一個數列表達式。比如現在有下面兩個隨機說發生器
//發生器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並發編程之美》