java編程學習:并發之ThreadLocalRandom源碼分析!

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 的原理,讓每個線程內持有一個本地的種子變量,

該種子變量只有在使用隨機數時候才會被初始化,多線程下計算新種子時候是根據自己線程內維護的種子變量進行更新,從而避免了競爭。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,914評論 2 372

推薦閱讀更多精彩內容