Random
JDK 提供了一个现成的 Random 类,在代码中生成随机数。
Random 类是线程安全的。
Random.next () 生成一个随机整数的实现
protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
oldseed = seed.get();
nextseed = (oldseed * multiplier + addend) & mask;
} while (!seed.compareAndSet(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}
注意:compareAndSet 比较并设置,有竞争是效率低下。
Random 是线程安全的,但是并不是 “高并发” 的。
ThreadLocalRandom
ThreadLocalRandom 提供了和 Random 相同的随机数生成功能,只是实现算法略有不同。
为了应对线程竞争,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;
消除伪共享
注解 @sun.misc.Contended,解决:伪共享。
原因:
CPU 是不直接访问内存的
数据都是从高速缓存中加载到寄存器的
CPU 读取和更新缓存的时候,是以行为单位进行的
一个 cache line,一行一般 64 字节,也就是 8 个 long 的长度。
一个缓存行可以放多个变量,如果多个线程同时访问的不同的变量,就产生了竞争
导致系统的性能大大折扣,这就是伪共享问题。
解决:
@sun.misc.Contended ("tlr") 就会在虚拟机层面,帮助我们在变量的前后生成一些 padding;
使得被标注的变量位于同一个缓存行,不与其它变量冲突。
在 Thread 对象中,这三个成员变量被标记为同一个组 tlr,使得这 3 个变量放置于一个单独的缓存行;
而不与其它变量发生冲突,从而提高在并发环境中的访问速度。
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 个字节的偏移量中。
避开了字段名,直接使用偏移量,就可以轻松绕过成员的可见性限制。
获取成员变量的偏移量:
// Unsafe mechanics
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 = sun.misc.Unsafe.getUnsafe();
Class<?> tk = Thread.class;
SEED = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSeed"));
PROBE = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomProbe"));
SECONDARY = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSecondarySeed"));
} catch (Exception e) {
throw new Error(e);
}
}
分别使用反射方式 byReflection () 和 Unsafe 的方式 byUnsafe () ;
来读写 threadLocalRandomSeed 变量 1 亿次
byUnsafe spend :171ms
byReflection spend :645ms
threadLocalRandomSeed
threadLocalRandomSeed 是使用最广泛的大量的随机数。
初始种子默认使用的是系统时间。
初始化的种子通过 UNSAFE 存在 SEED 的位置(即 threadLocalRandomSeed)。
接着就可以使用 nextInt () 方法获得随机整数。
每一次调用 nextInt () 都会使用 nextSeed () 更新 threadLocalRandomSeed。
由于这是一个线程独有的变量,因此完全不会有竞争;
也不会有 CAS 的重试,性能也就大大提高了。
探针 Probe
除了种子外,还有一个 threadLocalRandomProbe 探针变量。
针对每个 Thread 的 Hash 值(不为 0),它可以用来作为一个线程的特征值
基于这个值可以为线程在数组中找到一个特定的位置。
可以使用 ThreadLocalRandom.advanceProbe() 方法来修改一个线程的探针值,这样可以进一步避免未来可能得冲突