1. 程式人生 > 實用技巧 >Java高併發14-多執行緒下ThreadLcoalRandom原始碼解析以及對比

Java高併發14-多執行緒下ThreadLcoalRandom原始碼解析以及對比

一、複習

  • 公平鎖,非公平鎖,可重入鎖,自旋鎖,獨佔鎖和共享鎖

二、Java併發包中的ThreadLocalRandom類

1.起源以及優點

  • ThreadLocalRandom類是在JDK7的JUC包開始新增的類,彌補了Random類在高併發環境下的缺點

2.Random類以及侷限性

  • java.util.Random類是一種常用的隨機數生成器,在java.land.Math中的隨機數也是使用的Random的例項,下面先舉個例子
packagecom.ruigege.PricipleAnalyzingOfThreadLocalRandom3;

importjava.util.Random;

public
classRandomTest{

publicstaticvoidmain(String[]args){
Randomrandom=newRandom();
for(inti=0;i<5;i++){
System.out.println(random.nextInt(5));//輸出五個5以內的隨機數
}

}
}

14.1
  • 解析:建立一個使用預設種子的隨機數生成器,然後傳入一個限制5,讓其生成一個5以內的隨機整數。
  • 首先種子是什麼意思?這就要講到Random的原理,基本原理就是,我們可以通過函式傳入一個數字,或者使用無參建構函式建立例項(然而實際上程式碼內部會根據某個演算法生成一個預設的數字),然後根據傳入的引數或者內部自己生成的數字,作為起點,根據某些隨機演算法生成下一個數字,然後依次類推,下一個隨機數的生成是依賴於上一個隨機數的。
  • 我們看一下nextInt的原始碼
publicintnextInt(intbound){
//引數檢查
if(bound<0){
thrownewIllegalArgumentException(BadBound);
}
intr=next(31);
//根據新的種子來計算下一個種子
.......
returnr;
}
  • 解析:int r=next(31)相當於seed=f(seed)函式,利用31來生成一個隨機數r,然後返回r;下面的步驟省略了,其實可以抽象為g(seed,bound),保證生成的數字能夠在bound的範圍內。
  • 由於這種機制,每一個隨機數生成是依賴上一個隨機數的,因此在多執行緒的情況下,多個執行緒生成的隨機數都是相同的,這並不是我們希望得到的。下面看一下next的程式碼
protectedintnext(intbits){
longoldseed,nextseed;
AtomicLongseed=this.seed;
do{
oldseed=seed.get();//(1)
nextseed=(oldseed*multiplier+addend)&mask;//(2)
}while(!seed.compareAndSet(oldseed,nextseed));//(3)
return(int)(nextseed>>>(48-bits));//(4)
  • (1)代表獲取當前原子變數種子的值;(2)代表了一種演算法,這種演算法就是根據前一個種子變數來產生下一個種子變數;(3)其實是我們之前講過的CAS操作,保證在多執行緒的情況下依然能夠保證一致性,這個執行語句就是指一個執行緒判斷老種子是否和自己已知的種子是否一樣,如果一樣的話,那麼就設定舊種子為新種子值,如果不一致就會進入到迴圈之中,直至判斷為true,接著繼續下面的操作。這個語句是為了保證操作的一致性。;(4)拿到新種子,並且根據之前給的數值限制,來返回一個隨機數。

3.總結

  • 使用Random類的例項,基本原理就是定義一個原子性long變數,然後根據舊種子生成新的種子,最後返回隨機數。由於在多執行緒情況下,只有一個執行緒才能實現原子性變數的一系列操作,因為其他執行緒就是自旋,造成執行緒併發性大大降低。另外,多個執行緒獲取得隨機數都是一樣,那麼這樣真的"隨機”嗎?
  • 針對這種情況,出現了併發下的隨機數類ThreadLocalRandom

三、ThreadLocalRandom類

  • 這個類實現原理有些像ThradLocals變數,ThreadLocalRandom其實也是一個工具類,具體的隨機數變數是Thread類的內建成員變數。
  • 首先建立一個測試類
packagecom.ruigege.PricipleAnalyzingOfThreadLocalRandom3;

importjava.util.concurrent.ThreadLocalRandom;

publicclassThreadLocalRandomTest{

publicvoidmain(String[]args){
ThreadLocalRandomrandom=ThreadLocalRandom.current();
for(inti=0;i<10;i++){
System.out.println(random.nextInt(5));
}
}
}

14.2
  • 使用該例項輸出10個5以內的隨機數

1.基本原理

  • 與ThreadLocal類似,在Thread內部 維護一個隨機數的變數,然後通過該工具類去在每個執行緒中,保留一個副本(其實就是一個執行緒一套玩法),避免了對共享變數進行同步。Random的缺點就是多個執行緒會對同一個原子性種子變數,從而導致對原子變數的更新競爭。
  • 每個執行緒自己內部維護一個種子變數的副本,這樣多個執行緒執行緒就不對競爭原始的共享變量了,大大增強了併發性。

2.原始碼分析

  • 直接上UML圖來進行解釋
  • (1)繼承關係:ThreadLocalRandom繼承了Random類
  • (2)前三個成員變數,我們先不用關注,用到了再說
  • (3)instance變數是一個TheadLocalRandom例項,它是通過current()這個靜態方法來建立一個例項,然後返回,也就是說,我們測試的時候,用內建方法即可獲取例項,而不需要使用構造方法,來獲取例項。
  • (4)probeGenerator和seeder兩個原子性變數,是我們初始化呼叫執行緒的種子和探針變數的時候會用到它們,每個執行緒之後呼叫一次。
  • (5)nextInt方法,傳參限制數,然後生成下一個隨機數。
  • (6)nextSeed()方法根據上一個種子數值來生成下一個種子數值
  • (7)對於Thread類,裡面有一個非原子性變數threadLocalRandomSeed,這個就是一個執行緒自己一個種子變數,不和其他執行緒公用,這樣就能避免上面提到的共享變數衝突,有些類似於threadLocal變數,通過這個原理ThreadLocalRandom其實就一個工具類,內含一些和執行緒無關的通用演算法,具體變數會放到每個執行緒的例項中,所以它是執行緒安全的。

3.初始化變數的部分

privatestaticfinalsun.misc.UnsafeUNSAFE;
privatestaticfinallongSEED;
privatestaticfinallongPROBE;
privatestaticfinallongSECONDARY;

static{
try{
//使用Unsafe機制,獲取一個例項,並使用它來獲取成員變數的偏移量
UNSAFE=sun.misc.Unsafe.getUnsafe();
Class<?>thread=Thread.class;
//獲取thread內部成員變數的偏移量
SEED=UNSAFE.objectFieldOffset(thread.getDeclaredField("threadLocalRandomSeed"));
PROBE=UNSAFE.objectFieldOffset(thread.getDeclaredField("threadLocalRandomProbe"));
SECONDARY=UNSAFE.objectFieldOffset(thread.getDeclaredField("threadLocalRandomSecondary"));
}catch(Exceptione){
e.printStackTrace();
}
}
  • 基本看註釋即可

4.看一下current函式

staticfinalThreadLocalRandominstance=newThreadLcoalRandom();
publicstaticThreadLocalRandomcurrent(){
//這裡做一個判斷,也就是當前執行緒中的PROBE這個成員變數的值為0嗎?
//如果為0,說面這是第一次呼叫,我們先要進行初始化該例項,再返回。
if(UNSAFE.getInt(Thread.currentThread(),PROBE)==0){
localInit();
}
returninstance;
}

staticfinalvoidlocalInit(){
intp=probeGenerator.addAndGet(PROBE_INCREMENT);
intprobe=(p==0)?1:p;//跳過0
intseed=mix64(seeder.getAndAdd(SEEDER_INCREMENT));
Threadt=Thread.currentThread();
UNSAFE.putLong(t,SEED,seed);
UNSAFE.putInt(t,PROBE,probe);
}
  • 使用方法current獲取一個static型別的例項,各個執行緒是共用一個工具類例項,基本進入就是初始化,講各個變數賦值一下,這樣的設定,只用呼叫的時候才會初始化與隨機數有關的變數,這樣能夠提高效率,優化程式。

5.nextInt(int bound)方法

publicintnextInt(intbound){
//引數校驗
if(bound<0){
thrownewIllegalArgumentException(BadBound);
}
//根據當前執行緒中的種子來計算下一個種子
intr=mix32(nextSeed());
//下面就一個根據bound來返回一個隨機數的演算法了
intm=bound-1;
if((bound&m)==0){
r&=m;
}else{
for(intu=r>>>1;u+m-(r=u%bound)<0;u=mix32(nextSeed())>>>1);
}
returnr;
}
finallongnextSeed(){
Threadt=Thread.currentThread();
longr=UNSAFE.getLong(t,SEED)+GAMMA;
//將r的值,放到當前執行緒中SEED變數中
UNSAFE.putLong(t,SEED,r);
returnr;
}

四、原始碼: