Random和ThreadLocalRandom
在日常項(xiàng)目開(kāi)發(fā)中,隨機(jī)的場(chǎng)景需求經(jīng)常發(fā)生,如紅包、負(fù)載均衡等等。在 Java 中的,使用隨機(jī),一般使用 Random 或者 Math.random()。這篇文章中主要就來(lái)介紹下 Random,以及在并發(fā)環(huán)境下一些更好的選擇 ThreadLocalRandom。
一. Random
1.Random 使用
Random 類(lèi)位于 java.util 包下,是一種偽隨機(jī)。它主要提供了一下幾種不同類(lèi)型的隨機(jī)數(shù)接口:
nextBoolean(),boolean 類(lèi)型的隨機(jī),隨機(jī)返回 true/false
nextFloat(),float 類(lèi)型的隨機(jī),隨機(jī)返回 0.0 - 1.0 之間的 float 類(lèi)型
nextDouble(),double 類(lèi)型的隨機(jī),隨機(jī)返回 0.0 - 1.0 之間的 double 類(lèi)型
nextLong(),long 類(lèi)型的隨機(jī),隨機(jī)返回 long 類(lèi)型的隨機(jī)數(shù)
nextInt(),int 類(lèi)型的隨機(jī),隨機(jī)返回 int 類(lèi)型的隨機(jī)數(shù)
nextInt(int bound),同 nextInt(),但是返回的 int 上界是 bound,且不包括 bound,下界是 0
nextGaussian(),double 類(lèi)型的隨機(jī),隨機(jī)返回 0.0 - 1.0 之前的 double 類(lèi)型,但是它整體會(huì)表現(xiàn)出高斯分布
Random 中,十分關(guān)鍵的是 Seed,它是一個(gè) 48bit 的的隨機(jī)種子。換而言之,Random 的隨機(jī)是一種偽隨機(jī),它也是一套非常復(fù)雜的算法,生成的隨機(jī)數(shù)也是有規(guī)律可循。這套算法的執(zhí)行需要一個(gè)初始數(shù)值,在 Random 中初始值,就是 seed 隨機(jī)種子。對(duì)于相同的 Seed,調(diào)用隨機(jī)方法相同,得到的隨機(jī)數(shù)是一樣的。
接下來(lái),看一個(gè)使用 Random 實(shí)現(xiàn)隨機(jī)紅包的功能。輸入兩個(gè)參數(shù),第一個(gè)是紅包總金額,第二個(gè)是紅包個(gè)數(shù):
????/***?隨機(jī)紅包實(shí)現(xiàn)**?@param?totalAmount?紅包總金額*?@param?nums?紅包個(gè)數(shù)*?@author?huaijin*/static?List<Long>?randomRedEnvelope(Long?totalAmount,?int?nums)?{if?(nums?==?0)?{throw?new?RuntimeException("紅包個(gè)數(shù)需要大于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 之間的數(shù)字,作為紅包的占比,以達(dá)到隨機(jī)紅包的實(shí)現(xiàn)。
2.Random 線(xiàn)程安全保證
Random 在多線(xiàn)程的環(huán)境是并發(fā)安全的,它解決競(jìng)爭(zhēng)的方式是使用用原子類(lèi),本質(zhì)上上也就是 CAS + Volatile 保證線(xiàn)程安全。接下來(lái)就分析下其原理,以理解線(xiàn)程安全的實(shí)現(xiàn)。
在 Random 類(lèi)中,有一個(gè) AtomicLong 的域,用來(lái)保存隨機(jī)種子。其中每次生成隨機(jī)數(shù)時(shí)都會(huì)根據(jù)隨機(jī)種子做移位操作以得到隨機(jī)數(shù)。如 Long 類(lèi)型的隨機(jī):
long 類(lèi)型在 Java 中總弄 64bit,對(duì) next 方法的返回值左移 32 作為 long 的高位,然后將 next 方法返回值作為低 32 位,作為 long 類(lèi)型的隨機(jī)數(shù)。此處關(guān)鍵之處在于 next 方法,以下是 next 方法的核心
使用 seed 種子,不斷生成新的種子,然后使用 CAS 將其更新,再返回種子的移位后值。這里不斷的循環(huán) CAS 操作種子,直到成功。
可見(jiàn),Random 實(shí)現(xiàn)原理主要是利用隨機(jī)種子采用一定算法進(jìn)行處理生成隨機(jī)數(shù),在隨機(jī)種子的安全保證利用原子類(lèi) AtomicLong。
3. 并發(fā)下 Random 的不足
以上分析了 Random 的實(shí)現(xiàn)原理,雖然 Random 是線(xiàn)程安全,但是對(duì)于并發(fā)處理使用原子類(lèi) AtomicLong 在大量競(jìng)爭(zhēng)時(shí),由于很多 CAS 操作會(huì)造成失敗,不斷的 Spin,而造成 CPU 開(kāi)銷(xiāo)比較大而且吞吐量也會(huì)下降。
現(xiàn)在發(fā)現(xiàn)問(wèn)題就是大量的并發(fā)競(jìng)爭(zhēng),使得 CAS 失敗,對(duì)于競(jìng)爭(zhēng)問(wèn)題的優(yōu)化策略在前文 AtomicLong 和 LongAdder 時(shí),也談到了。鎖的極限優(yōu)化是 Free Lock,如 ThreadLoal 方式。
在 JDK 1.7 中由并發(fā)大神引入了 ThreadLocalRandom 來(lái)解決 Random 的大并發(fā)問(wèn)題,以下兩者的測(cè)試結(jié)果比較。每個(gè)線(xiàn)程生成 10w 次,運(yùn)行 12 次,去掉了最大最小值的平均結(jié)果:
可以看出 Random 隨著競(jìng)爭(zhēng)越來(lái)越激烈,然后耗時(shí)越來(lái)越多,說(shuō)明吞吐量將成倍下降。然而 ThreadLocalRandom 隨著線(xiàn)程數(shù)的增加,基本沒(méi)有變化。所以在大并發(fā)的情況下,隨機(jī)的選擇,可以考慮 ThreadLocalRandom 提升性能,也是性能優(yōu)化之道的一步。
二. 更好的選擇 ThreadLocalRandom
ThreadLocalRandom 是 Random 的子類(lèi),它是將 Seed 隨機(jī)種子隔離到當(dāng)前線(xiàn)程的隨機(jī)數(shù)生成器,從而解決了 Random 在 Seed 上競(jìng)爭(zhēng)的問(wèn)題,它的處理思想和 ThreadLocal 本質(zhì)相同。這里開(kāi)門(mén)見(jiàn)山,直接看源碼,分析其實(shí)現(xiàn)原理。
使用 ThreadLocalRandom 的方式為
“ThreadLocalRandom.current().nextX(...)
”其中 X 表示,Int、Long、Double、Float、Boolean 等等。按照這樣的方法調(diào)用逐步深入其中細(xì)節(jié):
從上述代碼中顯而意見(jiàn)就可以看出使用了單例模式,當(dāng) UNSAFE.getInt(Thread.currentThread(), PROBE) 返回 0 時(shí),就執(zhí)行 localInit(),否則就返回單例。
首先看單例 instance
從注釋中也可以看出,是一個(gè)公共的 ThreadLocalRandom,也就是說(shuō),在一個(gè) Java 應(yīng)用中只有一個(gè) ThreadLocalRandom 對(duì)象,顯然是單例,即無(wú)論哪個(gè)線(xiàn)程執(zhí)行隨機(jī)時(shí)都是使用這個(gè)單例對(duì)象。
那么單例情況下又如何將隨機(jī)種子隔離呢?
再來(lái)看下 UNSAFE.getInt(Thread.currentThread(), PROBE),這條語(yǔ)句主要是獲取當(dāng)前 Thread 對(duì)象中的 PROBE,再看看 PROBE 的初始化
PROBE 是 Thread 中 threadLocalRandomProbe
從注釋中可以看出,threadLocalRandomProbe 用于表示 ThreadLocalRandom 是否初始化,如果是非 0,表示其已經(jīng)初始化。換句話(huà)說(shuō),該變量就是狀態(tài)變量,用于標(biāo)識(shí) ThreadLocalRandom 是否被初始化。
其中還有個(gè)非常關(guān)鍵的 threadLocalRandomSeed,從注釋中也可以看出,它是當(dāng)前線(xiàn)程的隨機(jī)種子。到這里,一下子豁然開(kāi)朗,隨機(jī)種子分散在各個(gè) Thread 對(duì)象中,從而避免了并發(fā)時(shí)的競(jìng)爭(zhēng)點(diǎn)。
那么它又是什么時(shí)候初始化的呢?
當(dāng) Thread 對(duì)象被創(chuàng)建后,threadLocalRandomProbe 和 threadLocalRandomSeed 應(yīng)該都是 0。當(dāng)在這個(gè)線(xiàn)程中首次調(diào)用 ThreadLocalRandom.current 時(shí),threadLocalRandomProbe 為 0,會(huì)執(zhí)行 localInit。其中會(huì)初始化 threadLocalRandomSeed,并將 threadLocalRandomProbe 更新為非 0,表示已經(jīng)初始化。
上面核心的兩步驟,初始化 Thread 中的 threadLocalRandomSeed 和 threadLocalRandomProbe。
當(dāng) localInit 執(zhí)行后,就返回 ThreadLocalRandom 的單例供應(yīng)用使用 nextX() 系列方法生成隨機(jī)數(shù)。再來(lái)看下 nextInt 的實(shí)現(xiàn)
從以上可以看出,當(dāng)生成 int 隨機(jī)數(shù)時(shí),每次都利用 Unsafe 工具獲取當(dāng)前 Thread 對(duì)象中的隨機(jī)種子生成隨機(jī)數(shù)。并且每次獲取的時(shí)候,都將 Seed 種子增加 GAMMA,以供下次使用。
三. 總結(jié)
Random 是 Java 中提供的隨機(jī)數(shù)生成器工具類(lèi),但是在大并發(fā)的情況下由于其隨機(jī)種子的競(jìng)爭(zhēng)會(huì)導(dǎo)致吞吐量下降,從而引入 ThreadLocalRandom。它將競(jìng)爭(zhēng)點(diǎn)隔離到每個(gè)線(xiàn)程中,從而消除了大并發(fā)情況下競(jìng)爭(zhēng)問(wèn)題,提升了性能。
從兩者的設(shè)計(jì)上,可以看出在處理并發(fā)優(yōu)化時(shí)的優(yōu)秀設(shè)計(jì)思想,對(duì)于競(jìng)爭(zhēng)問(wèn)題,可以將將競(jìng)爭(zhēng)點(diǎn)隔離,如使用 ThreadLocal 實(shí)現(xiàn)。
并發(fā)競(jìng)爭(zhēng)的整體優(yōu)化思路,還是像前文中總結(jié)的一樣:
lock -> cas + volatile -> free lock
只會(huì) free lock 的設(shè)計(jì)方式就是避免競(jìng)爭(zhēng),將競(jìng)爭(zhēng)點(diǎn)隔離到線(xiàn)程中,從而解決競(jìng)爭(zhēng)。
作者:懷瑾握瑜
來(lái)源鏈接:
https://www.cnblogs.com/lxyit/p/12654374.html
總結(jié)
以上是生活随笔為你收集整理的Random和ThreadLocalRandom的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 服务器无法远程访问的原因有哪些
- 下一篇: BeanUtils如何优雅的拷贝List