Java高併發14-多線程下ThreadLcoalRandom源碼解析以及對比

一、複習

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

二、Java併發包中的ThreadLocalRandom類

1.起源以及優點

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

2.Random類以及侷限性

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

import java.util.Random;

public class RandomTest {

 public static void main(String[] args) {
  Random random = new Random();
  for(int i=0;i<5;i++) {
   System.out.println(random.nextInt(5));//輸出五個5以內的隨機數
  }
  
 }
}

14.1
14.1
  • 解析:創建一個使用默認種子的隨機數生成器,然後傳入一個限制5,讓其生成一個5以內的隨機整數。
  • 首先種子是什麼意思?這就要講到Random的原理,基本原理就是,我們可以通過函數傳入一個數字,或者使用無參構造函數創建實例(然而實際上代碼內部會根據某個算法生成一個默認的數字),然後根據傳入的參數或者內部自己生成的數字,作爲起點,根據某些隨機算法生成下一個數字,然後依次類推,下一個隨機數的生成是依賴於上一個隨機數的。
  • 我們看一下nextInt的源碼
public int nextInt(int bound){
 //參數檢查
 if(bound<0){
  throw new IllegalArgumentException(BadBound);
 }
 int r=next(31);
 //根據新的種子來計算下一個種子
 .......
 return r;
}
  • 解析:int r=next(31)相當於seed=f(seed)函數,利用31來生成一個隨機數r,然後返回r;下面的步驟省略了,其實可以抽象爲g(seed,bound),保證生成的數字能夠在bound的範圍內。
  • 由於這種機制,每一個隨機數生成是依賴上一個隨機數的,因此在多線程的情況下,多個線程生成的隨機數都是相同的,這並不是我們希望得到的。下面看一下next的代碼
protected int next(int bits){
 long oldseed,nextseed;
 AtomicLong seed = 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類的內置成員變量。
  • 首先創建一個測試類
package com.ruigege.PricipleAnalyzingOfThreadLocalRandom3;

import java.util.concurrent.ThreadLocalRandom;

public class ThreadLocalRandomTest {

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

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

1.基本原理

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

2.源碼分析

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

3.初始化變量的部分

 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<?> thread = Thread.class;
   //獲取thread內部成員變量的偏移量
   SEED = UNSAFE.objectFieldOffset(thread.getDeclaredField("threadLocalRandomSeed"));
   PROBE = UNSAFE.objectFieldOffset(thread.getDeclaredField("threadLocalRandomProbe"));
   SECONDARY = UNSAFE.objectFieldOffset(thread.getDeclaredField("threadLocalRandomSecondary"));
  }catch(Exception e) {
   e.printStackTrace();
  }
 }
  • 基本看註釋即可

4.看一下current函數

 static final ThreadLocalRandom instance = new ThreadLcoalRandom();
 public static ThreadLocalRandom current() {
  //這裏做一個判斷,也就是當前線程中的PROBE這個成員變量的值爲0嗎?
  //如果爲0,說面這是第一次調用,我們先要進行初始化該實例,再返回。
  if(UNSAFE.getInt(Thread.currentThread(),PROBE)==0) {
   localInit();
  }
  return instance;
 }
 
 static final void localInit() {
  int p = probeGenerator.addAndGet(PROBE_INCREMENT);
  int probe = (p==0)?1:p;//跳過0
  int seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
  Thread t = Thread.currentThread();
  UNSAFE.putLong(t, SEED,seed);
  UNSAFE.putInt(t, PROBE,probe);
 }
  • 使用方法current獲取一個static類型的實例,各個線程是共用一個工具類實例,基本進入就是初始化,講各個變量賦值一下,這樣的設置,只用調用的時候纔會初始化與隨機數有關的變量,這樣能夠提高效率,優化程序。

5.nextInt(int bound)方法

 public int nextInt(int bound) {
  //參數校驗
  if(bound<0) {
   throw new IllegalArgumentException(BadBound);
  }
  //根據當前線程中的種子來計算下一個種子
  int r = mix32(nextSeed());
  //下面就一個根據bound來返回一個隨機數的算法了
  int m = bound-1;
  if((bound & m)==0) {
   r&= m;
  }else {
   for (int u=r>>>1;u+m-(r=u%bound)<0;u = mix32(nextSeed())>>>1);
  }
  return r;
 }
 final long nextSeed() {
  Thread t = Thread.currentThread();
  long r = UNSAFE.getLong(t,SEED)+GAMMA;
  //將r的值,放到當前線程中SEED變量中
  UNSAFE.putLong(t, SEED,r);
  return r;
 }

四、源碼:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章