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以内的随机数
  }
  
 }
}

Java高并发14-多线程下ThreadLcoalRandom源码解析以及对比
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));
  }
 }
}

Java高并发14-多线程下ThreadLcoalRandom源码解析以及对比
14.2
  • 使用该实例输出10个5以内的随机数

1.基本原理

  • 与ThreadLocal类似,在Thread内部 维护一个随机数的变量,然后通过该工具类去在每个线程中,保留一个副本(其实就是一个线程一套玩法),避免了对共享变量进行同步。Random的缺点就是多个线程会对同一个原子性种子变量,从而导致对原子变量的更新竞争。
  • 每个线程自己内部维护一个种子变量的副本,这样多个线程线程就不对竞争原始的共享变量了,大大增强了并发性。

2.源码分析

  • 直接上UML图来进行解释 Java高并发14-多线程下ThreadLcoalRandom源码解析以及对比
  • (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;
 }

四、源码: