Java Review - 并发编程_ThreadLocalRandom实现原理源码分析
文章目錄
- 概述
- Random的局限性
- ThreadLocalRandom使用及原理
- 使用
- 原理
- ThreadLocalRandom源碼分析
- ThreadLocalRandom current() 該方
- int nextInt(int bound)
- 小結
概述
ThreadLocalRandom類是JDK 7在JUC包下新增的隨機數生成器,它彌補了Random類在多線程下的缺陷。我們這里主要講解為何要在JUC下新增該類,以及該類的實現原理。
Random的局限性
在JDK 7之前包括現在,java.util.Random都是使用比較廣泛的隨機數生成工具類,而且java.lang.Math中的隨機數生成也使用的是java.util.Random的實例。
import java.util.Random;/*** @author 小工匠* @version 1.0* @description: TODO* @date 2021/11/28 23:05* @mark: show me the code , change the world*/ public class RandomTest {public static void main(String[] args) {// 1Random random = new Random();for (int i = 0; i < 10; i++) {// 2System.out.println(random.nextInt(5));}} }-
代碼(1)創建一個默認隨機數生成器,并使用默認的種子。
-
代碼(2)輸出10個在0~5(包含0,不包含5)之間的隨機數。
隨機數的生成需要一個默認的種子,這個種子其實是一個long類型的數字,你可以在創建Random對象時通過構造函數指定,如果不指定則在默認構造函數內部生成一個默認的值。
有了默認的種子后,如何生成隨機數呢?
public int nextInt(int bound) {// 3 參數檢查if (bound <= 0)throw new IllegalArgumentException(BadBound);// 4 根據老的種子生成新的種子 int r = next(31);// 5 根據新的種子計算隨機數int m = bound - 1;if ((bound & m) == 0) // i.e., bound is a power of 2r = (int)((bound * (long)r) >> 31);else {for (int u = r;u - (r = u % bound) + m < 0;u = next(31));}return r;}由此可見,新的隨機數的生成需要兩個步驟:
-
首先根據老的種子生成新的種子。
-
然后根據新的種子來計算新的隨機數。
-
步驟(4)我們可以抽象為seed=f(seed),其中f是一個固定的函數,比如seed=f(seed)=a*seed+b;
-
步驟(5)也可以抽象為g(seed,bound),其中g是一個固定的函數,比如g(seed,bound)=(int)((bound* (long)seed) >> 31)。
在單線程情況下每次調用nextInt都是根據老的種子計算出新的種子,這是可以保證隨機數產生的隨機性的。
但是在多線程下多個線程可能都拿同一個老的種子去執行步驟(4)以計算新的種子,這會導致多個線程產生的新種子是一樣的,由于步驟(5)的算法是固定的,所以會導致多個線程產生相同的隨機值,這并不是我們想要的。
步驟(4)要保證原子性,也就是說當多個線程根據同一個老種子計算新種子時,第一個線程的新種子被計算出來后,第二個線程要丟棄自己老的種子,而使用第一個線程的新種子來計算自己的新種子,依此類推,只有保證了這個,才能保證在多線程下產生的隨機數是隨機的。
Random函數使用一個原子變量達到了這個效果,在創建Random對象時初始化的種子就被保存到了種子原子變量里面,下面看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)獲取當前原子變量種子的值。
-
代碼(7)根據當前種子值計算新的種子。
-
代碼(8)使用CAS操作,它使用新的種子去更新老的種子,在多線程下可能多個線程都同時執行到了代碼(6),那么可能多個線程拿到的當前種子的值是同一個,然后執行步驟(7)計算的新種子也都是一樣的,但是步驟(8)的CAS操作會保證只有一個線程可以更新老的種子為新的,失敗的線程會通過循環重新獲取更新后的種子作為當前種子去計算老的種子,這就解決了上面提到的問題,保證了隨機數的隨機性。
-
代碼(9)使用固定算法根據新的種子計算隨機數。
總結:每個Random實例里面都有一個原子性的種子變量用來記錄當前的種子值,當要生成新的隨機數時需要根據當前種子計算新的種子并更新回原子變量。在多線程下使用單個Random實例生成隨機數時,當多個線程同時計算隨機數來計算新的種子時,多個線程會競爭同一個原子變量的更新操作,由于原子變量的更新是CAS操作,同時只有一個線程會成功,所以會造成大量線程進行自旋重試,這會降低并發性能,所以ThreadLocalRandom應運而生。
ThreadLocalRandom使用及原理
為了彌補多線程高并發情況下Random的缺陷,在JUC包下新增了ThreadLocalRandom類.
使用
import java.util.concurrent.ThreadLocalRandom;/*** @author 小工匠* @version 1.0* @description: TODO* @date 2021/11/28 23:28* @mark: show me the code , change the world*/ public class ThreadLocalRandomTest {public static void main(String[] args) {// 10 獲取一個隨機數生成器ThreadLocalRandom tr = ThreadLocalRandom.current();// 11 輸出10個(包括0 不包括5)之間的隨機數for (int i = 0; i < 10; i++) {System.out.println(tr.nextInt(5));}} }- 代碼(10)調用ThreadLocalRandom.current()來獲取當前線程的隨機數生成器
原理
從名字上看它會讓我們聯想到ThreadLocal:ThreadLocal通過讓每一個線程復制一份變量,使得在每個線程對變量進行操作時實際是操作自己本地內存里面的副本,從而避免了對共享變量進行同步。
實際上ThreadLocalRandom的實現也是這個原理,Random的缺點是多個線程會使用同一個原子性種子變量,從而導致對原子變量更新的競爭.
那么,如果每個線程都維護一個種子變量,則每個線程生成隨機數時都根據自己老的種子計算新的種子,并使用新種子更新老的種子,再根據新種子計算隨機數,就不會存在競爭問題了,這會大大提高并發性能。
ThreadLocalRandom源碼分析
-
ThreadLocalRandom類繼承了Random類并重寫了nextInt方法,在ThreadLocalRandom類中并沒有使用繼承自Random類的原子性種子變量。
-
在ThreadLocalRandom中并沒有存放具體的種子,具體的種子存放在具體的調用線程的threadLocalRandomSeed變量里面。
-
ThreadLocalRandom類似于ThreadLocal類,就是個工具類。當線程調用ThreadLocalRandom的current方法時,ThreadLocalRandom負責初始化調用線程的threadLocalRandomSeed變量,也就是初始化種子。
-
當調用ThreadLocalRandom的nextInt方法時,實際上是獲取當前線程的threadLocalRandomSeed變量作為當前種子來計算新的種子,然后更新新的種子到當前線程的threadLocalRandomSeed變量,而后再根據新種子并使用具體算法計算隨機數。
這里需要注意的是,threadLocalRandomSeed變量就是Thread類里面的一個普通long變量,它并不是原子性變量。其實道理很簡單,因為這個變量是線程級別的,所以根本不需要使用原子性變量,如果你還是不理解可以思考下ThreadLocal的原理。
其中seeder和probeGenerator是兩個原子性變量,在初始化調用線程的種子和探針變量時會用到它們,每個線程只會使用一次。
另外,變量instance是ThreadLocalRandom的一個實例,該變量是static的。當多線程通過ThreadLocalRandom的current方法獲取ThreadLocalRandom的實例時,其實獲取的是同一個實例。但是由于具體的種子是存放在線程里面的,所以在ThreadLocalRandom的實例里面只包含與線程無關的通用算法,所以它是線程安全的。
ThreadLocalRandom current() 該方
法獲取ThreadLocalRandom實例,并初始化調用線程中的threadLocalRandomSeed和threadLocalRandomProbe變量。
/*** Returns the current thread's {@code ThreadLocalRandom}.** @return the current thread's {@code ThreadLocalRandom}*/public static ThreadLocalRandom current() {// 12 if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)// 13 localInit();// 14return instance;} /*** Initialize Thread fields for the current thread. Called only* when Thread.threadLocalRandomProbe is zero, indicating that a* thread local seed value needs to be generated. Note that even* though the initialization is purely thread-local, we need to* rely on (static) atomic generators to initialize the values.*/static final void localInit() {int p = probeGenerator.addAndGet(PROBE_INCREMENT);int probe = (p == 0) ? 1 : p; // skip 0long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));Thread t = Thread.currentThread();UNSAFE.putLong(t, SEED, seed);UNSAFE.putInt(t, PROBE, probe);}在如上代碼(12)中,如果當前線程中threadLocalRandomProbe的變量值為0(默認情況下線程的這個變量值為0),則說明當前線程是第一次調用ThreadLocalRandom的current方法,那么就需要調用localInit方法計算當前線程的初始化種子變量。這里為了延遲初始化,在不需要使用隨機數功能時就不初始化Thread類中的種子變量,這是一種優化。
代碼(13)首先根據probeGenerator計算當前線程中threadLocalRandomProbe的初始化值,然后根據seeder計算當前線程的初始化種子,而后把這兩個變量設置到當前線程。代碼(14)返回ThreadLocalRandom的實例。需要注意的是,這個方法是靜態方法,多個線程返回的是同一個ThreadLocalRandom實例。
int nextInt(int bound)
public int nextInt(int bound) {// 15 if (bound <= 0)throw new IllegalArgumentException(BadBound);// 16 int r = mix32(nextSeed());// 17 int m = bound - 1;if ((bound & m) == 0) // power of twor &= m;else { // reject over-represented candidatesfor (int u = r >>> 1;u + m - (r = u % bound) < 0;u = mix32(nextSeed()) >>> 1);}return r;}如上代碼的邏輯步驟與Random相似,我們重點看下nextSeed()方法。
final long nextSeed() {Thread t; long r; // read and update per-thread seedUNSAFE.putLong(t = Thread.currentThread(), SEED,r = UNSAFE.getLong(t, SEED) + GAMMA);return r;}首先使用 r = UNSAFE.getLong(t, SEED)獲取當前線程中threadLocalRandomSeed變量的值,然后在種子的基礎上累加GAMMA值作為新種子,而后使用UNSAFE的putLong方法把新種子放入當前線程的threadLocalRandomSeed變量中。
小結
我們這里主要闡述了Random的實現原理以及Random在多線程下需要競爭種子原子變量更新操作的缺點,從而引出ThreadLocalRandom類。
ThreadLocalRandom使用ThreadLocal的原理,讓每個線程都持有一個本地的種子變量,該種子變量只有在使用隨機數時才會被初始化。
在多線程下計算新種子時是根據自己線程內維護的種子變量進行更新,從而避免了競爭。
總結
以上是生活随笔為你收集整理的Java Review - 并发编程_ThreadLocalRandom实现原理源码分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java Review - 并发编程_伪
- 下一篇: Java Review - 并发编程_原