下面是Random.next()天生一个随机整数的实现:

protected int next(int bits) { long oldseed, nextseed; AtomicLong seed = this.seed; do { oldseed = seed.get(); nextseed = (oldseed multiplier + addend) & mask; //CAS 有竞争是效率低下 } while (!seed.compareAndSet(oldseed, nextseed)); return (int)(nextseed >>> (48 - bits)); }

不丢脸到,上面的方法中利用CAS操作更新seed,在大量线程竞争的场景下,这个CAS操作很可能失落败,失落败了就会重试,而这个重试又会花费CPU运算,从而使得性能大大低落了。

因此,虽然Random是线程安全的,但是并不是“高并发”的。

php高并发随机函数高并发情形下你还在用Random生成随机数 NoSQL

为了改进这个问题,增强随机数天生器在高并发环境中的性能,于是乎,就有了ThreadLocalRandom——一个性能刁悍的高并发随机数天生器。

ThreadLocalRandom继续自Random,根据里氏代换原则,这解释ThreadLocalRandom供应了和Random相同的随机数生成功能,只是实现算法略有不同。

在Thread中的变量

为了应对线程竞争,Java中有一个ThreadLocal类,为每一个线程分配了一个独立的,互不相关的存储空间。

ThreadLocal的实现依赖于Thread工具中的ThreadLocal.ThreadLocalMap threadLocals成员字段。

与之类似,为了让随机数天生器只访问本地线程数据,从而避免竞争,在Thread中,又增加了3个成员:

/ The current seed for a ThreadLocalRandom / @sun.misc.Contended("tlr") long threadLocalRandomSeed; / Probe hash value; nonzero if threadLocalRandomSeed initialized / @sun.misc.Contended("tlr") int threadLocalRandomProbe; / Secondary seed isolated from public ThreadLocalRandom sequence / @sun.misc.Contended("tlr") int threadLocalRandomSecondarySeed;

这3个字段作为Thread类的成员,便自然和每一个Thread工具牢牢得捆绑在一起,因此成为了名副实在的ThreadLocal变量,而依赖这几个变量实现的随机数天生器,也就成为了ThreadLocalRandom。

肃清伪共享

不知道大家有没有把稳到, 在这些变量上面,都带有一个表明@sun.misc.Contended,这个表明是干什么用的呢?要理解这个,大家得先知道一下并发编程中的一个主要问题——伪共享:

我们知道,CPU是不直接访问内存的,数据都是从高速缓存中加载到寄存器的,高速缓存又有L1,L2,L3等层级。
在这里,我们先简化这些卖力的层级关系,假设只有一级缓存和一个主内存。

CPU读取和更新缓存的时候,因此行为单位进行的,也叫一个cache line,一行一样平常64字节,也便是8个long的长度。

因此,问题就来了,一个缓存行可以放多个变量,如果多个线程同时访问的不同的变量,而这些不同的变量又恰好位于同一个缓存行,那会发生什么呢?

如上图所示,X,Y为相邻2个变量,位于同一个缓存行,两个CPU core1 core2都加载了他们,core1更新X,同时,core2更新Y,由于数据的读取和更新因此缓存行为单位的,这就意味着当这2件事同时发生时,就产生了竞争,导致core1和core2有可能须要重新刷新自己的数据(缓存行被对方更新了),这就导致系统的性能大大折扣,这便是伪共享问题。

那怎么改进呢?如下图:

上图中,我们把X单独占用一个缓存行,Y单独占用一个缓存行,这样各自更新和读取,都不会有任何影响了。

而上述代码中的@sun.misc.Contended("tlr")就会在虚拟机层面,帮助我们在变量的前后天生一些padding,使得被标注的变量位于同一个缓存行,不与其它变量冲突。

在Thread工具中,成员变量threadLocalRandomSeed,threadLocalRandomProbe,threadLocalRandomSecondarySeed被标记为同一个组tlr,使得这3个变量放置于一个单独的缓存行,而不与其它变量发生冲突,从而提高在并发环境中的访问速率。

反射的高效替代方案

随机数的产生须要访问Thread的threadLocalRandomSeed等成员,但是考虑到类的封装性,这些成员却是包内可见的。

很不幸,ThreadLocalRandom位于java.util.concurrent包,而Thread则位于java.lang包,因此,ThreadLocalRandom并没有办法访问Thread的threadLocalRandomSeed等变量。

这时,Java老鸟们可能就会跳出来说:这算什么,看我的反射大法,不管啥都能抠出来访问一下。

说的不错,反射是一种可以绕过封装,直接访问工具内部数据的方法,但是,反射的性能不太好,并不适宜作为一个高性能的办理方案。

有没有什么办法可以让ThreadLocalRandom访问Thread的内部成员,同时又具有远超于反射的,且无限靠近于直接变量访问的方法呢?答案是肯定的,这便是利用Unsafe类。

这里,就大略先容一下用的两个Unsafe的方法:

public native long getLong(Object o, long offset);public native void putLong(Object o, long offset, long x);

个中getLong()方法,会读取工具o的第offset字节偏移量的一个long型数据;putLong()则会将x写入工具o的第offset个字节的偏移量中。

这类类似C的操作方法,带来了极大的性能提升,更主要的是,由于它避开了字段名,直策应用偏移量,就可以轻松绕过成员的可见性限定了。

性能问题办理了,那下一个问题是,我怎么知道threadLocalRandomSeed成员在Thread中的偏移位置呢,这就须要用unsafe的objectFieldOffset()方法了,请看下面的代码:

上述这段static代码,在ThreadLocalRandom类初始化的时候,就取得了Thread成员变量threadLocalRandomSeed,threadLocalRandomProbe,threadLocalRandomSecondarySeed在工具偏移中的位置。

因此,只要ThreadLocalRandom须要利用这些变量,都可以通过unsafe的getLong()和putLong()来进行访问(也可能是getInt()和putInt())。

比如在天生一个随机数的时候:

protected int next(int bits) { return (int)(mix64(nextSeed()) >>> (64 - bits)); } final long nextSeed() { Thread t; long r; // read and update per-thread seed //在ThreadLocalRandom中,访问了Thread的threadLocalRandomSeed变量 UNSAFE.putLong(t = Thread.currentThread(), SEED, r = UNSAFE.getLong(t, SEED) + GAMMA); return r; }

这种Unsafe的方法掉地能有多快呢,让我们一起看做个试验看看:

这里,我们自己写一个ThreadTest类,利用反射和unsafe两种方法,来一直读写threadLocalRandomSeed成员变量,比较它们的性能差异,代码如下:

上述代码中,分别利用反射办法byReflection() 和Unsafe的办法byUnsafe()来读写threadLocalRandomSeed变量1亿次,得到的测试结果如下:

byUnsafe spend :171msbyReflection spend :645ms

不丢脸到,利用Unsafe的方法远远优于反射的方法,这也是JDK内部,大量利用Unsafe来替代反射的缘故原由之一。

随机数种子

我们知道,伪随机数天生都须要一个种子,threadLocalRandomSeed和threadLocalRandomSecondarySeed便是这里的种子。
个中threadLocalRandomSeed是long型的,threadLocalRandomSecondarySeed是int。

threadLocalRandomSeed是利用最广泛的大量的随机数实在都是基于threadLocalRandomSeed的。
而threadLocalRandomSecondarySeed只是某些特定的JDK内部实现中有利用,利用并不广泛。

初始种子默认利用的是系统韶光:

上述代码中完成了种子的初始化,并将初始化的种子通过UNSAFE存在SEED的位置(即threadLocalRandomSeed)。

接着就可以利用nextInt()方法得到随机整数了:

public int nextInt() { return mix32(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; }

每一次调用nextInt()都会利用nextSeed()更新threadLocalRandomSeed。
由于这是一个线程独占的变量,因此完备不会有竞争,也不会有CAS的重试,性能也就大大提高了。

探针Probe的浸染

除了种子外,还有一个threadLocalRandomProbe探针变量,这个变量是用来做什么的呢?

我们可以把threadLocalRandomProbe 理解为一个针对每个Thread的Hash值(不为0),它可以用来作为一个线程的特色值,基于这个值可以为线程在数组中找到一个特定的位置。

static final int getProbe() { return UNSAFE.getInt(Thread.currentThread(), PROBE);}

来看一个代码片段:

CounterCell[] as; long b, s; if ((as = counterCells) != null || !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { CounterCell a; long v; int m; boolean uncontended = true; if (as == null || (m = as.length - 1) < 0 || // 利用probe,为每个线程找到一个在数组as中的位置 // 由于每个线程的probe值不一样,因此大概率 每个线程对应的数组中的元素也是不一样的 // 每个线程对应了不同的元素,就可以没有冲突的进行完备的并发操作 // 因此探针probe在这里 就起到了防止冲突的浸染 (a = as[ThreadLocalRandom.getProbe() & m]) == null || !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {

在详细的实现中,如果上述代码发生了冲突,那么,还可以利用ThreadLocalRandom.advanceProbe()方法来修正一个线程的探针值,这样可以进一步避免未来可能得冲突,从而减少竞争,提高并发性能。

static final int advanceProbe(int probe) { //根据当前探针值,打算一个更新的探针值 probe ^= probe << 13; // xorshift probe ^= probe >>> 17; probe ^= probe << 5; //更新探针值到线程工具中 即修正了threadLocalRandomProbe变量 UNSAFE.putInt(Thread.currentThread(), PROBE, probe); return probe; }