深入浅析Random类在高并发下的缺陷及JUC对其的优化

 更新时间:2019年04月24日 17:52:42   作者:CodeBear  
这篇文章主要介绍了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类型的参数,这个参数就被称为“种子”,这样“种子”就固定了,生成的随机数也都是一样了:

让我们简单的看下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循环:

3.1 线程A,利用CAS算法,发现seed的值还是为oldseed,说明seed的值没有被替换过,就把seed的值替换成第二步生成出来的nextseed,替换成功,返回true,!true为false,退出while循环。
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来计算新的种子的,这样就避免了竞争的问题。

相关文章

  • Spring中使用copyProperties方法进行对象之间的属性赋值详解

    Spring中使用copyProperties方法进行对象之间的属性赋值详解

    这篇文章主要介绍了Spring中使用copyProperties方法进行对象之间的属性赋值详解,使用org.springframework.beans.BeanUtils.copyProperties方法进行对象之间属性的赋值,避免通过get、set方法一个一个属性的赋值,需要的朋友可以参考下
    2023-12-12
  • JAVA实现生成短链接的示例代码

    JAVA实现生成短链接的示例代码

    短链接就是将长度较长的链接压缩成较短的链接,本文就来介绍一下JAVA实现生成短链接的示例代码,具有一定的参考价值,感兴趣的可以了解一下
    2023-08-08
  • httpclient的监控流程源码解读

    httpclient的监控流程源码解读

    这篇文章主要为大家介绍了httpclient的监控流程源码解读,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-10-10
  • 读取Java文件到byte数组的三种方法(总结)

    读取Java文件到byte数组的三种方法(总结)

    下面小编就为大家带来一篇读取Java文件到byte数组的三种方法(总结)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-08-08
  • Java中的内部类你了解吗

    Java中的内部类你了解吗

    这篇文章主要为大家介绍了Java的内部类,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-01-01
  • idea每次修改代码都需要重新install的问题

    idea每次修改代码都需要重新install的问题

    这篇文章主要介绍了idea每次修改代码都需要重新install的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-08-08
  • Springboot集成阿里云OSS上传文件系统教程

    Springboot集成阿里云OSS上传文件系统教程

    这篇文章主要介绍了Springboot集成阿里云OSS上传文件系统教程,通过详细的图文展示,代码步骤的展示和文件配置信息,希望对你有所帮助
    2021-06-06
  • Java中的自旋锁spinlock详解

    Java中的自旋锁spinlock详解

    这篇文章主要介绍了Java中的自旋锁spinlock详解,自旋锁就是循环尝试获取锁,不会放弃CPU时间片,减伤cup上下文切换,缺点是循环会消耗cpu,需要的朋友可以参考下
    2024-01-01
  • Springboot如何使用OSHI获取和操作系统和硬件信息

    Springboot如何使用OSHI获取和操作系统和硬件信息

    这篇文章主要介绍了Springboot如何使用OSHI获取和操作系统和硬件信息问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-10-10
  • SpringBoot 之启动流程详解

    SpringBoot 之启动流程详解

    SpringBoot 是一个基于 Spring 框架的快速开发框架,旨在简化 Spring 应用程序的开发和部署。在本文中,我们将深入分析 SpringBoot 启动过程的源代码,并提供必要的解释和说明
    2023-04-04

最新评论