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生成的随机数,并非真正的随机,它有一个种子的概念,是根据种子值来计算【下一个】值的,若是种子值相同,那么它生成出来的随机数也一定相等,也就是“肯定的输入产生肯定的输出”。并发
若是你不信的话,咱们能够来作一个实验:dom
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));//一个神秘的算法 }
咱们能够看到核心就在第四步,我再来更详细的的描述下,首先要知道seed的类型:性能
private final AtomicLong seed;
seed的类型是AtomicLong,是一个原子操做类,能够保证原子性,seed.get就是得到seed具体的值,seed就是咱们所谓的种子,也就是种子值保存在了原子变量里面。学习
当有两个线程同时进入到next方法,会发生以下的事情:
看起来一切很美好,其实否则,若是并发很高,会发生什么?大量的线程都在进行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来计算新的种子的,这样就避免了竞争的问题。
若是想学习Java工程化、高性能及分布式、深刻浅出。微服务、Spring,MyBatis,Netty源码分析的朋 友能够加个人Java高级交流:787707172,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视 频免费分享给你们。