Random能夠說是每一個開發都知道,並且都用的很6的類,若是你說,你沒有用過Random,也不知道Random是什麼鬼,那麼你也不會來到這個技術類型的社區,也看不到個人博客了。但並非每一個人都知道Random的原理,知道Random在高併發下的缺陷的人應該更少。這篇博客,我就來分析下Random類在併發下的缺陷以及JUC對其的優化。算法
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生成的隨機數,並非真正的隨機,它有一個種子的概念,是根據種子值來計算【下一個】值的,若是種子值相同,那麼它生成出來的隨機數也一定相等,也就是「肯定的輸入產生肯定的輸出」。編程
若是你不信的話,咱們能夠來作一個實驗:bash
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的源碼把,源碼涉及到算法,固然算法不是本篇博客討論的重點,咱們能夠把源碼簡化成以下的樣子:dom
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));//一個神祕的算法
}
複製代碼
咱們能夠看到核心就在第四步,我再來更詳細的的描述下,首先要知道seed的類型:學習
private final AtomicLong seed;
複製代碼
seed的類型是AtomicLong,是一個原子操做類,能夠保證原子性,seed.get就是得到seed具體的值,seed就是咱們所謂的種子,也就是種子值保存在了原子變量裏面。 當有兩個線程同時進入到next方法,會發生以下的事情:優化
看起來一切很美好,其實否則,若是併發很高,會發生什麼?大量的線程都在進行while循環,這是至關佔用CPU的,因此JUC推出了ThreadLocalRandom來解決這個問題。ui
首先,讓咱們來看看ThreadLocalRandom的使用方法:this
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
System.out.println(threadLocalRandom.nextInt(100));
}
}
複製代碼
能夠看到使用方式發生了些許的改變,咱們來看看ThreadLocalRandom核心代碼的實現邏輯:
public static ThreadLocalRandom current() {
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
複製代碼
有一點須要注意,因爲current是一個靜態的方法,因此屢次調用此方法,返回的ThreadLocalRandom對象是同一個。
若是當前線程的PROBE是0,說明是第一次調用current方法,那麼須要調用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設置到當前的線程,這樣其餘線程就拿不到了。
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方法作了什麼:
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來計算新的種子的,這樣就避免了競爭的問題。