在日常項目開發中,隨機的場景需求經常發生,如紅包、負載均衡等等。在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的設計方式就是避免競爭,將競爭點隔離到線程中,從而解決競爭。
