Java是一種可以撰寫跨平臺應用軟件的面向對象的程序設計語言。Java 技術具有卓越的通用性、高效性、平臺移植性和安全性,廣泛應用于PC、數據中心、游戲控制臺、科學超級計算機、移動電話和互聯網,同時擁有全球最大的開發者專業社群。
給你學習路線:html-css-js-jq-javase-數據庫-jsp-servlet-Struts2-hibernate-mybatis-spring4-springmvc-ssh-ssm
JDK 并發包中 ThreadLocalRandom 類原理剖析,經常使用的隨機數生成器 Random 類的原理是什么?及其局限性是什么?ThreadLocalRandom 是如何利用 ThreadLocal 的原理來解決 Random 的局限性?
我們首先看Random 類及其局限性,如下:
在 JDK7 之前包括現在,java.util.Random 應該是使用比較廣泛的隨機數生成工具類,另外 java.lang.Math 中的隨機數生成也是使用的 java.util.Random 的實例。下面先看看 java.util.Random 的使用例子如下:
/** * Created by cong on 2018/6/4. */public class RandomTest { public static void main(String[] args) { //(1)創建一個默認種子的隨機數生成器 Random random = new Random(); //(2)輸出10個在0-5(包含0,不包含5)之間的隨機數 for (int i = 0; i < 10; ++i) { System.out.println(random.nextInt(5)); } }}
代碼(1)創建一個默認隨機數生成器,使用默認的種子。
代碼(2)輸出輸出10個在0-5(包含0,不包含5)之間的隨機數。
運行結果如下:
小編推薦一個學Java的學習裙【 七六零,二五零,五四一 】,無論你是大牛還是小白,是想轉行還是想入行都可以來了解一起進步一起學習!裙內有開發工具,很多干貨和技術資料分享!
這里提下隨機數的生成需要一個默認的種子,這個種子實際上就是是一個 long 類型的數字,這個種子要么在 Random 的時候通過構造函數指定,那么默認構造函數內部會生成一個默認的值,問題來了,有了默認的種子后,如何生成隨機數呢?
我們進入Radom類里面去看nextInt方法的源碼,如下:
public int nextInt(int var1) { //(3)參數校驗 if(var1 <= 0) { throw new IllegalArgumentException("bound must be positive"); } else { //(4)根據老的種子生成心的種子 int var2 = this.next(31); //(5)以下根據新的種子計算隨機數 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; } }
可以看到上面代碼可知新的隨機數的生成需要兩個步驟:
1.首先需要根據老的種子生成新的種子。
2.然后根據新的種子來計算新的隨機數。
其中步驟(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 應運而生。
為了解決多線程高并發下 Random 的缺陷,JUC 包下新增了 ThreadLocalRandom 類。我首先先看 ThreadLocalRandom的原理,如下圖:
接著我們再看一下ThreadLocalRandom 的類圖結構,如下圖:
小編推薦一個學Java的學習裙【 七六零,二五零,五四一 】,無論你是大牛還是小白,是想轉行還是想入行都可以來了解一起進步一起學習!裙內有開發工具,很多干貨和技術資料分享!
可知 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 的主要代碼實現邏輯,如下:
首先是Unsafe機制的使用,具體以后再講。如下源碼:
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實例 UNSAFE = sun.misc.Unsafe.getUnsafe(); Class tk = Thread.class; //獲取Thread類里面threadLocalRandomSeed變量在Thread實例里面偏移量 SEED = UNSAFE.objectFieldOffset (tk.getDeclaredField("threadLocalRandomSeed")); //獲取Thread類里面threadLocalRandomProbe變量在Thread實例里面偏移量 PROBE = UNSAFE.objectFieldOffset (tk.getDeclaredField("threadLocalRandomProbe")); //獲取Thread類里面threadLocalRandomProbe變量在Thread實例里面偏移量,這個值在后面講解的LongAdder里面會用到 SECONDARY = UNSAFE.objectFieldOffset (tk.getDeclaredField("threadLocalRandomSecondarySeed")); } catch (Exception e) { throw new Error(e); } }
ThreadLocalRandom current() 方法:該方法獲取 ThreadLocalRandom 實例,并初始化調用線程中 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)如果當前線程中 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)根據新種子和bound計算隨機數 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 相似,我們重點看下 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) 獲取當前線程中 threadLocalRandomSeed 變量的值,然后在種子的基礎上累加 GAMMA 值作為新種子,然后使用 UNSAFE 的 putLong 方法把新種子放入當前線程的 threadLocalRandomSeed 變量。
理論知道了,那么現在用一個例子來講解 ThreadLocalRandom 如何使用,例子如下:
public class RandomTest { public static void main(String[] args) { //(10)獲取一個隨機數生成器 ThreadLocalRandom random = ThreadLocalRandom.current(); //(11)輸出10個在0-5(包含0,不包含5)之間的隨機數 for (int i = 0; i < 10; ++i) { System.out.println(random.nextInt(5)); } }}
運行結果如下:
如上代碼(10)調用 ThreadLocalRandom.current() 來獲取當前線程的隨機數生成器。
ThreadLocal 的出現就是為了解決多線程下變量的隔離問題,讓每一個線程拷貝一份變量,每個線程對變量進行操作時候實際是操作自己本地內存里面的拷貝。
實際上 ThreadLocalRandom 的實現也是這個原理,Random 的缺點是多個線程會使用原子性種子變量,會導致對原子變量更新的競爭,如下圖:
小編推薦一個學Java的學習裙【 七六零,二五零,五四一 】,無論你是大牛還是小白,是想轉行還是想入行都可以來了解一起進步一起學習!裙內有開發工具,很多干貨和技術資料分享!
如果每個線程維護自己的一個種子變量,每個線程生成隨機數時候根據自己老的種子計算新的種子,并使用新種子更新老的種子,然后根據新種子計算隨機數,就不會存在競爭問題,這會大大提高并發性能。這就是ThreadLocalRandom使用ThreadLocal的原理的獨到之處。
到目前為止,我們知道了 Random 的實現原理以及介紹了 Random 在多線程下存在競爭種子原子變量更新操作失敗后自旋等待的缺點,從而引出 ThreadLocalRandom 類,ThreadLocalRandom 使用 ThreadLocal 的原理,讓每個線程內持有一個本地的種子變量,
該種子變量只有在使用隨機數時候才會被初始化,多線程下計算新種子時候是根據自己線程內維護的種子變量進行更新,從而避免了競爭。