1. 程式人生 > >Random在高併發下的缺陷以及JUC對其的優化

Random在高併發下的缺陷以及JUC對其的優化

Random可以說是每個開發都知道,而且都用的很6的類,如果你說,你沒有用過Random,也不知道Random是什麼鬼,那麼你也不會來到這個技術型別的社群,也看不到我的部落格了。但並不是每個人都知道Random的原理,知道Random在高併發下的缺陷的人應該更少。這篇部落格,我就來分析下Random類在併發下的缺陷以及JUC對其的優化。

Random的原理及缺陷

public static void main(String[] args) {
 Random random = new Random();
 System.out.println(random.nextInt(100));
 }

在學習程式設計的時候,我一直對JDK開發人員很不解:為什麼產生隨機數的方法名是:“”nextXXX”?雖然我英語只停留“點頭yes,搖頭no,來是come,去是go” 的水平,但是我知道next是“下一個”的意思,如果我來命名,會命名為“create”,“generate”,這樣不是更“貼切”嗎?為什麼JDK開發人員會命名為“nextXXX”呢?難道是他們突然“詞窮”了,想不出什麼單詞了,所以乾脆隨便來一個?後來才知道,原來通過Random生成的隨機數,並不是真正的隨機,它有一個種子的概念,是根據種子值來計算【下一個】值的,如果種子值相同,那麼它生成出來的隨機數也必定相等,也就是“確定的輸入產生確定的輸出”。

如果你不信的話,我們可以來做一個實驗:

public static void main(String[] args) {
 for (int i = 0; i < 10; i++) {
 Random random = new Random(15);
 System.out.println(random.nextInt(100));
 }
 }

Random類除了提供無參的構造方法以外,還提供了有參的構造方法,我們可以傳入int型別的引數,這個引數就被稱為“種子”,這樣“種子”就固定了,生成的隨機數也都是一樣了:

41
41
41
41
41
41
41
41
41
41

讓我們簡單的看下nextInt的原始碼把,原始碼涉及到演算法,當然演算法不是本篇部落格討論的重點,我們可以把原始碼簡化成如下的樣子:

public int nextInt(int bound) {
 if (bound <= 0)
 throw new IllegalArgumentException(BadBound);
 //1.根據老的種子生成新的種子
 int r = next(31);
 //2.根據新的種子計算隨機數
 ...
 return r;
 }

首先是根據老的種子生成新的種子,然後是根據新的種子計算出隨機數,nextXXX的核心程式碼可以被簡化這兩步。

現在讓我們想一個問題,如果在高併發的情況下,有N個執行緒,同時執行到第一步:根據老的種子生成新的種子,獲得的種子不就一樣了嗎?由於第二步是根據新的種子來計算隨機數,這個演算法又是固定的,會產生什麼情況?N個執行緒最終獲得的隨機數不都一樣了嗎?顯然這不是我們想要的,所以JDK開發人員想到了這點,讓我們看看next方法裡面做了什麼:

protected int next(int bits) {
 long oldseed, nextseed;//定義舊種子,下一個種子
 AtomicLong seed = this.seed;
 do {
 oldseed = seed.get();//獲得舊的種子值,賦值給oldseed 
 nextseed = (oldseed * multiplier + addend) & mask;//一個神祕的演算法
 } while (!seed.compareAndSet(oldseed, nextseed));//CAS,如果seed的值還是為oldseed,就用nextseed替換掉,並且返回true,退出while迴圈,如果已經不為oldseed了,就返回false,繼續迴圈
 return (int)(nextseed >>> (48 - bits));//一個神祕的演算法
 }
  1. 定義了舊種子oldseed,下一個種子(新種子)nextseed。
  2. 獲得舊的種子的值,賦值給oldseed 。
  3. 一個神祕的演算法,計算出下一個種子(新種子)賦值給nextseed。
  4. 使用CAS操作,如果seed的值還是oldseed,就用nextseed替換掉,並且返回true,!true為false,退出while迴圈;如果seed的值已經不為oldseed了,就說明seed的值已經被替換過了,返回false,!false為true,繼續下一次while迴圈。
  5. 一個神祕的演算法,根據nextseed計算出隨機數,並且返回。

我們可以看到核心就在第四步,我再來更詳細的的描述下,首先要知道seed的型別:

private final AtomicLong seed;

seed的型別是AtomicLong,是一個原子操作類,可以保證原子性,seed.get就是獲得seed具體的值,seed就是我們所謂的種子,也就是種子值儲存在了原子變數裡面。

當有兩個執行緒同時進入到next方法,會發生如下的事情:

  1. 執行緒A,執行緒B同時拿到了seed的值,賦值給oldseed變數。
  2. 根據一個神祕的演算法,計算出nextseed為XXX。注意,既然這個演算法是固定的,那麼生成出來的nextseed也必定是固定的。
  3. 進入while迴圈:
  4. 3.1 執行緒A,利用CAS演算法,發現seed的值還是為oldseed,說明seed的值沒有被替換過,就把seed的值替換成第二步生成出來的nextseed,替換成功,返回true,!true為false,退出while迴圈。
  5. 3.2 執行緒B,利用CAS演算法,發現seed的值已經不為oldseed了,因為執行緒A已經把seed的值替換成了nextseed了啊,所以CAS失敗,只能再次迴圈。再次迴圈的時候, seed.get()就拿到了最新的種子值,再次根據一個神祕的演算法獲得了nextSeed,CAS成功,退出迴圈。

看起來一切很美好,其實不然,如果併發很高,會發生什麼?大量的執行緒都在進行while迴圈,這是相當佔用CPU的,所以JUC推出了ThreadLocalRandom來解決這個問題。

ThreadLocalRandom

首先,讓我們來看看ThreadLocalRandom的使用方法:

public static void main(String[] args) {
 for (int i = 0; i < 10; i++) {
 ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
 System.out.println(threadLocalRandom.nextInt(100));
 }
 }

可以看到使用方式發生了些許的改變,我們來看看ThreadLocalRandom核心程式碼的實現邏輯:

current

public static ThreadLocalRandom current() {
 if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
 localInit();
 return instance;
 }

有一點需要注意,由於current是一個靜態的方法,所以多次呼叫此方法,返回的ThreadLocalRandom物件是同一個。

如果當前執行緒的PROBE是0,說明是第一次呼叫current方法,那麼需要呼叫localInit方法,否則直接返回已經產生的例項。

localInit

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);
 }

首先初始化probe和seed,隨後呼叫UNSAFE類的方法,把probe和seed設定到當前的執行緒,這樣其他執行緒就拿不到了。

nextInt

public int nextInt(int bound) {
 if (bound <= 0)
 throw new IllegalArgumentException(BadBound);
 int r = mix32(nextSeed());
 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類下的nextXXX方法的原理一樣,也是根據舊的種子生成新的種子,然後根據新的種子來生成隨機數,我們來看下nextSeed方法做了什麼:

nextSeed

final long nextSeed() {
 Thread t; long r; // read and update per-thread seed
 UNSAFE.putLong(t = Thread.currentThread(), SEED,
 r = UNSAFE.getLong(t, SEED) + GAMMA);
 return r;
 }

首先使用UNSAFE.getLong(t, SEED) 來獲得當前執行緒的SEED,隨後+上GAMMA來作為新的種子值,隨後將新的種子值放到當前執行緒中。

總結

本文首先簡單的分析了Random的實現原理,引出nextXXX方法在高併發下的缺陷:需要競爭種子原子變數。接著介紹了ThreadLocalRandom的使用方法以及原理,從類的命名,就可以看出實現原理類似於ThreadLocal,seed種子是儲存在每個執行緒中的,也是根據每個執行緒中的seed來計算新的種子的,這樣就避免了競爭的問題。

如果想學習Java工程化、高效能及分散式、深入淺出。微服務、Spring,MyBatis,Netty原始碼分析的朋
友可以加我的Java高階交流:787707172,群裡有阿里大牛直播講解技術,以及Java大型網際網路技術的視
頻免