• 欢迎访问web前端中文站,JavaScript,CSS3,HTML5,web前端demo
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏web前端中文站吧

Random在高并发下的缺陷以及JUC对其的优化

JavaScript web前端中文站 6个月前 (04-24) 218次浏览 已收录 0个评论

Random 可以说是每个开发都知道,而且都用的很 6 的类,如果你说,你没有用过 Random,也不知道 Random 是什么鬼,那么你也不会来到这个技术类型的社区,也看不到我的博客了。但并不是每个人都知道 Random 的原理,知道 Random 在高并发下的缺陷的人应该更少。这篇博客,我就来分析下 Random 类在并发下的缺陷以及 JUC 对其的优化。

更多精彩内容请看 web 前端中文站
http://www.lisa33xiaoq.net 可按 Ctrl + D 进行收藏

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 类型的参数,这个参数就被称为“种子”,这样“种子”就固定了,生成的随机数也都是一样了:

41 41 41 41 41 41 41 41 41 41

让我们简单的看下 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 来计算新的种子的,这样就避免了竞争的问题。

【注:本文源自网络文章资源,由站长整理发布】


web 前端中文站 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:Random 在高并发下的缺陷以及 JUC 对其的优化
喜欢 (0)
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址