java編程學(xué)習(xí):并發(fā)之ThreadLocalRandom源碼分析!

Java是一種可以撰寫(xiě)跨平臺(tái)應(yīng)用軟件的面向?qū)ο蟮某绦蛟O(shè)計(jì)語(yǔ)言。Java 技術(shù)具有卓越的通用性、高效性、平臺(tái)移植性和安全性,廣泛應(yīng)用于PC、數(shù)據(jù)中心、游戲控制臺(tái)、科學(xué)超級(jí)計(jì)算機(jī)、移動(dòng)電話和互聯(lián)網(wǎng),同時(shí)擁有全球最大的開(kāi)發(fā)者專業(yè)社群。

給你學(xué)習(xí)路線:html-css-js-jq-javase-數(shù)據(jù)庫(kù)-jsp-servlet-Struts2-hibernate-mybatis-spring4-springmvc-ssh-ssm

JDK 并發(fā)包中 ThreadLocalRandom 類(lèi)原理剖析,經(jīng)常使用的隨機(jī)數(shù)生成器 Random 類(lèi)的原理是什么?及其局限性是什么?ThreadLocalRandom 是如何利用 ThreadLocal 的原理來(lái)解決 Random 的局限性?

我們首先看Random 類(lèi)及其局限性,如下:

在 JDK7 之前包括現(xiàn)在,java.util.Random 應(yīng)該是使用比較廣泛的隨機(jī)數(shù)生成工具類(lèi),另外 java.lang.Math 中的隨機(jī)數(shù)生成也是使用的 java.util.Random 的實(shí)例。下面先看看 java.util.Random 的使用例子如下:

/** * Created by cong on 2018/6/4. */public class RandomTest { public static void main(String[] args) { //(1)創(chuàng)建一個(gè)默認(rèn)種子的隨機(jī)數(shù)生成器 Random random = new Random(); //(2)輸出10個(gè)在0-5(包含0,不包含5)之間的隨機(jī)數(shù) for (int i = 0; i < 10; ++i) { System.out.println(random.nextInt(5)); } }}

代碼(1)創(chuàng)建一個(gè)默認(rèn)隨機(jī)數(shù)生成器,使用默認(rèn)的種子。

代碼(2)輸出輸出10個(gè)在0-5(包含0,不包含5)之間的隨機(jī)數(shù)。

運(yùn)行結(jié)果如下:

小編推薦一個(gè)學(xué)Java的學(xué)習(xí)裙【 七六零,二五零,五四一 】,無(wú)論你是大牛還是小白,是想轉(zhuǎn)行還是想入行都可以來(lái)了解一起進(jìn)步一起學(xué)習(xí)!裙內(nèi)有開(kāi)發(fā)工具,很多干貨和技術(shù)資料分享!

這里提下隨機(jī)數(shù)的生成需要一個(gè)默認(rèn)的種子,這個(gè)種子實(shí)際上就是是一個(gè) long 類(lèi)型的數(shù)字,這個(gè)種子要么在 Random 的時(shí)候通過(guò)構(gòu)造函數(shù)指定,那么默認(rèn)構(gòu)造函數(shù)內(nèi)部會(huì)生成一個(gè)默認(rèn)的值,問(wèn)題來(lái)了,有了默認(rèn)的種子后,如何生成隨機(jī)數(shù)呢?

我們進(jìn)入Radom類(lèi)里面去看nextInt方法的源碼,如下:

public int nextInt(int var1) {    //(3)參數(shù)校驗(yàn) if(var1 <= 0) { throw new IllegalArgumentException("bound must be positive"); } else {       //(4)根據(jù)老的種子生成心的種子 int var2 = this.next(31);       //(5)以下根據(jù)新的種子計(jì)算隨機(jī)數(shù) int var3 = var1 - 1; if((var1 & var3) == 0) { var2 = (int)((long)var1 * (long)var2 >> 31); } else { for(int var4 = var2; var4 - (var2 = var4 % var1) + var3 < 0; var4 = this.next(31)) { ; } } return var2; } }

可以看到上面代碼可知新的隨機(jī)數(shù)的生成需要兩個(gè)步驟:

1.首先需要根據(jù)老的種子生成新的種子。

2.然后根據(jù)新的種子來(lái)計(jì)算新的隨機(jī)數(shù)。

其中步驟(4)我們可以抽象為 seed=f(seed),其中 f 是一個(gè)固定的函數(shù),比如 seed= f(seed)=a*seed+b;,

步驟(5)也可以抽象為 g(seed,bound),其中 g 是一個(gè)固定的函數(shù),比如 g(seed,bound)=(int)((bound * (long)seed) >> 31);。在單線程情況下每次調(diào)用 nextInt 都是根據(jù)老的種子計(jì)算出來(lái)新的種子,這是可以保證隨機(jī)數(shù)產(chǎn)生的隨機(jī)性的。

但是在多線程下多個(gè)線程可能都拿同一個(gè)老的種子去執(zhí)行步驟(4)計(jì)算新的種子,這會(huì)導(dǎo)致多個(gè)線程產(chǎn)生的新種子是一樣的,由于步驟(5)算法是固定的,所以會(huì)導(dǎo)致多個(gè)線程產(chǎn)生相同的隨機(jī)值,這并不是我們想要的。

所以需要保證步驟(4)的原子性,也就是說(shuō)多個(gè)線程在根據(jù)同一個(gè)老種子計(jì)算新種子時(shí)候,第一個(gè)線程的新種子計(jì)算出來(lái)后,第二個(gè)線程要丟棄自己老的種子,要使用第一個(gè)線程的新種子來(lái)計(jì)算自己的新種子,依次類(lèi)推,只有保證了這個(gè),才能保證多線程下產(chǎn)生的隨機(jī)數(shù)是隨機(jī)的。

Random 函數(shù)使用一個(gè)原子變量達(dá)到了這個(gè)效果,在創(chuàng)建 Random 對(duì)象時(shí)候初始化的種子就保存到了種子原子變量里面,下面看下 next() 的源碼:

protected int next(int bits) { long oldseed, nextseed; AtomicLong seed = this.seed; do { //(6) oldseed = seed.get(); //(7) nextseed = (oldseed * multiplier + addend) & mask; //(8) } while (!seed.compareAndSet(oldseed, nextseed)); //(9) return (int)(nextseed >>> (48 - bits)); }

代碼(6)獲取當(dāng)前原子變量種子的值;

代碼(7)根據(jù)當(dāng)前種子值計(jì)算新的種子;

代碼(8)使用 CAS 操作,使用新的種子去更新老的種子,多線程下可能多個(gè)線程都同時(shí)執(zhí)行到了代碼(6),那么可能多個(gè)線程都拿到的當(dāng)前種子的值是同一個(gè),然后執(zhí)行步驟(7)計(jì)算的新種子也都是一樣的,但是步驟(8)的 CAS 操作會(huì)保證只有一個(gè)線程可以更新老的種子為新的,

失敗的線程會(huì)通過(guò)循環(huán)重新獲取更新后的種子作為當(dāng)前種子去計(jì)算老的種子,可見(jiàn)這里解決了上面提到的問(wèn)題,也就保證了隨機(jī)數(shù)的隨機(jī)性。

代碼(9)則使用固定算法根據(jù)新的種子計(jì)算隨機(jī)數(shù)。

因此,每個(gè) Random 實(shí)例里面有一個(gè)原子性的種子變量用來(lái)記錄當(dāng)前的種子的值,當(dāng)要生成新的隨機(jī)數(shù)時(shí)候要根據(jù)當(dāng)前種子計(jì)算新的種子并更新回原子變量。多線程下使用單個(gè) Random 實(shí)例生成隨機(jī)數(shù)時(shí)候,多個(gè)線程同時(shí)計(jì)算新的種子時(shí)候會(huì)競(jìng)爭(zhēng)同一個(gè)原子變量的更新操作,、

由于原子變量的更新是 CAS 操作,同時(shí)只有一個(gè)線程會(huì)成功,所以會(huì)造成大量線程進(jìn)行自旋重試,這是會(huì)降低并發(fā)性能的,所以 ThreadLocalRandom 應(yīng)運(yùn)而生。

為了解決多線程高并發(fā)下 Random 的缺陷,JUC 包下新增了 ThreadLocalRandom 類(lèi)。我首先先看 ThreadLocalRandom的原理,如下圖:

接著我們?cè)倏匆幌耇hreadLocalRandom 的類(lèi)圖結(jié)構(gòu),如下圖:

小編推薦一個(gè)學(xué)Java的學(xué)習(xí)裙【 七六零,二五零,五四一 】,無(wú)論你是大牛還是小白,是想轉(zhuǎn)行還是想入行都可以來(lái)了解一起進(jìn)步一起學(xué)習(xí)!裙內(nèi)有開(kāi)發(fā)工具,很多干貨和技術(shù)資料分享!

可知 ThreadLocalRandom 繼承了 Random 并重寫(xiě)了 nextInt 方法,ThreadLocalRandom 中并沒(méi)有使用繼承自 Random 的原子性種子變量。

ThreadLocalRandom 中并沒(méi)有具體存放種子,具體的種子是存放到具體的調(diào)用線程的 threadLocalRandomSeed 變量里面的,ThreadLocalRandom 類(lèi)似于 ThreadLocal類(lèi) 就是個(gè)工具類(lèi)。

當(dāng)線程調(diào)用 ThreadLocalRandom 的 current 方法時(shí)候 ThreadLocalRandom 負(fù)責(zé)初始化調(diào)用線程的 threadLocalRandomSeed 變量,也就是初始化種子。

當(dāng)調(diào)用 ThreadLocalRandom 的 nextInt 方法時(shí)候,實(shí)際上是獲取當(dāng)前線程的 threadLocalRandomSeed 變量作為當(dāng)前種子來(lái)計(jì)算新的種子,然后更新新的種子到當(dāng)前線程的 threadLocalRandomSeed 變量,然后在根據(jù)新種子和具體算法計(jì)算隨機(jī)數(shù)。

這里需要注意的是 threadLocalRandomSeed 變量就是 Thread 類(lèi)里面的一個(gè)普通 long 變量,并不是原子性變量,其實(shí)道理很簡(jiǎn)單,因?yàn)檫@個(gè)變量是線程級(jí)別的,根本不需要使用原子性變量,如果還是不理解可以思考下 ThreadLocal 的原理。

其中變量 seeder 和 probeGenerator 是兩個(gè)原子性變量,在初始化調(diào)用線程的種子和探針變量時(shí)候用到,每個(gè)線程只會(huì)使用一次。

另外變量 instance 是個(gè) ThreadLocalRandom 的一個(gè)實(shí)例,該變量是 static 的,當(dāng)多線程通過(guò) ThreadLocalRandom 的 current 方法獲取 ThreadLocalRandom 的實(shí)例時(shí)候其實(shí)獲取的是同一個(gè),但是由于具體的種子是存放到線程里面的,

所以 ThreadLocalRandom 的實(shí)例里面只是與線程無(wú)關(guān)的通用算法,所以是線程安全的。

接下來(lái)進(jìn)入ThreadLocalRandom 的主要代碼實(shí)現(xiàn)邏輯,如下:

首先是Unsafe機(jī)制的使用,具體以后再講。如下源碼:

 private static final sun.misc.Unsafe UNSAFE; private static final long SEED; private static final long PROBE; private static final long SECONDARY; static { try { //獲取unsafe實(shí)例 UNSAFE = sun.misc.Unsafe.getUnsafe(); Class tk = Thread.class; //獲取Thread類(lèi)里面threadLocalRandomSeed變量在Thread實(shí)例里面偏移量 SEED = UNSAFE.objectFieldOffset (tk.getDeclaredField("threadLocalRandomSeed")); //獲取Thread類(lèi)里面threadLocalRandomProbe變量在Thread實(shí)例里面偏移量 PROBE = UNSAFE.objectFieldOffset (tk.getDeclaredField("threadLocalRandomProbe")); //獲取Thread類(lèi)里面threadLocalRandomProbe變量在Thread實(shí)例里面偏移量,這個(gè)值在后面講解的LongAdder里面會(huì)用到 SECONDARY = UNSAFE.objectFieldOffset (tk.getDeclaredField("threadLocalRandomSecondarySeed")); } catch (Exception e) { throw new Error(e); } }

ThreadLocalRandom current() 方法:該方法獲取 ThreadLocalRandom 實(shí)例,并初始化調(diào)用線程中 threadLocalRandomSeed 和 threadLocalRandomProbe 變量。源碼如下:

  static final ThreadLocalRandom instance = new ThreadLocalRandom(); public static ThreadLocalRandom current() { //(12) if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0) //(13) localInit(); //(14) return instance; } static final void localInit() { int p = probeGenerator.addAndGet(PROBE_INCREMENT); int probe = (p == 0) ? 1 : p; // skip 0 long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT)); Thread t = Thread.currentThread(); UNSAFE.putLong(t, SEED, seed); UNSAFE.putInt(t, PROBE, probe); }

代碼(12)如果當(dāng)前線程中 threadLocalRandomProbe 變量值為0(默認(rèn)情況下線程的這個(gè)變量為0),說(shuō)明當(dāng)前線程第一次調(diào)用 ThreadLocalRandom 的 current 方法,那么就需要調(diào)用 localInit 方法計(jì)算當(dāng)前線程的初始化種子變量。這里設(shè)計(jì)為了延遲初始化,

不需要使用隨機(jī)數(shù)功能時(shí)候 Thread 類(lèi)中的種子變量就不需要被初始化,這是一種優(yōu)化。

代碼(13)首先計(jì)算根據(jù) probeGenerator 計(jì)算當(dāng)前線程中 threadLocalRandomProbe 的初始化值,然后根據(jù) seeder 計(jì)算當(dāng)前線程的初始化種子,然后把這兩個(gè)變量設(shè)置到當(dāng)前線程。

代碼(14)返回 ThreadLocalRandom 的實(shí)例,需要注意的是這個(gè)方法是靜態(tài)方法,多個(gè)線程返回的是同一個(gè) ThreadLocalRandom 實(shí)例。

int nextInt(int bound) 方法:計(jì)算當(dāng)前線程的下一個(gè)隨機(jī)數(shù)。源碼如下圖所示:

public int nextInt(int bound) { //(15)參數(shù)校驗(yàn) if (bound <= 0) throw new IllegalArgumentException(BadBound); //(16) 根據(jù)當(dāng)前線程中種子計(jì)算新種子 int r = mix32(nextSeed()); //(17)根據(jù)新種子和bound計(jì)算隨機(jī)數(shù) int m = bound - 1; if ((bound & m) == 0) // power of two r &= m; else { // reject over-represented candidates for (int u = r >>> 1; u + m - (r = u % bound) < 0; u = mix32(nextSeed()) >>> 1) ; } return r; }

可以看到上面代碼邏輯步驟與 Random 相似,我們重點(diǎn)看下 nextSeed() 方法:

  final long nextSeed() { Thread t; long r; // UNSAFE.putLong(t = Thread.currentThread(), SEED,r = UNSAFE.getLong(t, SEED) + GAMMA); return r; }

如上代碼首先使用 r = UNSAFE.getLong(t, SEED) 獲取當(dāng)前線程中 threadLocalRandomSeed 變量的值,然后在種子的基礎(chǔ)上累加 GAMMA 值作為新種子,然后使用 UNSAFE 的 putLong 方法把新種子放入當(dāng)前線程的 threadLocalRandomSeed 變量。

理論知道了,那么現(xiàn)在用一個(gè)例子來(lái)講解 ThreadLocalRandom 如何使用,例子如下:

public class RandomTest { public static void main(String[] args) { //(10)獲取一個(gè)隨機(jī)數(shù)生成器 ThreadLocalRandom random = ThreadLocalRandom.current(); //(11)輸出10個(gè)在0-5(包含0,不包含5)之間的隨機(jī)數(shù) for (int i = 0; i < 10; ++i) { System.out.println(random.nextInt(5)); } }}

運(yùn)行結(jié)果如下:

如上代碼(10)調(diào)用 ThreadLocalRandom.current() 來(lái)獲取當(dāng)前線程的隨機(jī)數(shù)生成器。

ThreadLocal 的出現(xiàn)就是為了解決多線程下變量的隔離問(wèn)題,讓每一個(gè)線程拷貝一份變量,每個(gè)線程對(duì)變量進(jìn)行操作時(shí)候?qū)嶋H是操作自己本地內(nèi)存里面的拷貝。

實(shí)際上 ThreadLocalRandom 的實(shí)現(xiàn)也是這個(gè)原理,Random 的缺點(diǎn)是多個(gè)線程會(huì)使用原子性種子變量,會(huì)導(dǎo)致對(duì)原子變量更新的競(jìng)爭(zhēng),如下圖:

小編推薦一個(gè)學(xué)Java的學(xué)習(xí)裙【 七六零,二五零,五四一 】,無(wú)論你是大牛還是小白,是想轉(zhuǎn)行還是想入行都可以來(lái)了解一起進(jìn)步一起學(xué)習(xí)!裙內(nèi)有開(kāi)發(fā)工具,很多干貨和技術(shù)資料分享!

如果每個(gè)線程維護(hù)自己的一個(gè)種子變量,每個(gè)線程生成隨機(jī)數(shù)時(shí)候根據(jù)自己老的種子計(jì)算新的種子,并使用新種子更新老的種子,然后根據(jù)新種子計(jì)算隨機(jī)數(shù),就不會(huì)存在競(jìng)爭(zhēng)問(wèn)題,這會(huì)大大提高并發(fā)性能。這就是ThreadLocalRandom使用ThreadLocal的原理的獨(dú)到之處。

到目前為止,我們知道了 Random 的實(shí)現(xiàn)原理以及介紹了 Random 在多線程下存在競(jìng)爭(zhēng)種子原子變量更新操作失敗后自旋等待的缺點(diǎn),從而引出 ThreadLocalRandom 類(lèi),ThreadLocalRandom 使用 ThreadLocal 的原理,讓每個(gè)線程內(nèi)持有一個(gè)本地的種子變量,

該種子變量只有在使用隨機(jī)數(shù)時(shí)候才會(huì)被初始化,多線程下計(jì)算新種子時(shí)候是根據(jù)自己線程內(nèi)維護(hù)的種子變量進(jìn)行更新,從而避免了競(jìng)爭(zhēng)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容