Ramdom在多线程环境下的问题
首先我们看一下Random类的nextInt
源码:
//产生[0,bound)的随机数
public int nextInt(int bound) {
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
//生成一个31位的随机数
int r = next(31);
int m = bound - 1;
// 如果bound是2的整数幂次方
if ((bound & m) == 0) // i.e., bound is a power of 2
r = (int)((bound * (long)r) >> 31);
// 通过除余运算产生一个小于bound的值
else {
for (int u = r;
u - (r = u % bound) + m < 0;
u = next(31))
;
}
return r;
}
可以看出,产生随机数的核心逻辑在于next
方法中,并且实际上Random类的其他常用方法都是基于next
方法。
那么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));
}
可以看出,Random类的内部维护了一个随机数种子,并且类型为AtomicLong,那为什么要用AtomicLong
呢?
可以这样想:在多线程环境下,多个线程同时调用next
方法,同时获取旧种子并使用固定的算法生成新种子。那么每个线程产生的新种子的值自然是相同的,产生的随机数也是相同的,这当然是不希望看到的。
于是,Random类将种子定义为AtomicLong
类型,并通过CAS自旋锁的方式去更改随机数种子的值,若多个线程同时获取旧种子的值,并产生相同的新种子,但只要有一个线程通过CAS设置了新种子的值,后面的线程就无法通过CAS进行更改,必须重新获取种子的值并生成新种子。
通过CAS自旋之后,多线程下的问题是解决了,但是却产生了效率问题:多个线程争抢一个原子变量,只有一个线程会成功,而其他线程会在CAS上自旋重试,影响了并发性能。
ThreadLocalRandom分析
ThreadLocalRandom是JDK1.7为了解决Random并发效率问题产生的类,先来看看ThreadLocalRandom怎么用:
public static void main(String[] args) {
Runnable runnable = ()->{
// 通过静态方法获取ThreadLocalRandom对象
ThreadLocalRandom random = ThreadLocalRandom.current();
System.out.println(random.nextInt(100));
};
for(int i = 0;i<10;i++){
new Thread(runnable).start();
}
}
ThreadLocalRandom.current()
获取的到底是不是同一个对象呢?看看源码:
public static ThreadLocalRandom current() {
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
PROBE
是什么东西呢?可以在ThreadLocalRandom中找到如下静态字段和静态代码块:
// 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);
}
}
UNSAFE.objectFieldOffset
这个方法就厉害了,是获取指定的变量在所属类中的内存偏移地址,查看Thread
源码,可以看到这些字段:
/** 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;
其中threadLocalRandomSeed
是线程持有的随机数种子,threadLocalRandomProbe
可以范围为探针,大概是用来辅助的,threadLocalRandomSecondarySeed
是备用的随机数种子。
@sun.misc.Contended
注解是避免伪共享问题进行字段填充。
于是回到current
方法:
public static ThreadLocalRandom current() {
//getInt是通过偏移地址获取对象中的字段值
//如果为0说明还没初始化过,进行初始化
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
这里注意到其实return的对象是一个内部的静态对象,是单例的:
static final ThreadLocalRandom instance = new ThreadLocalRandom();
那么localInit
方法初始化的是什么呢?联系上面看到的Thread中的字段可以猜想也许是初始化线程中自己持有的Seed、Probe等:
static final void localInit() {
int p = probeGenerator.addAndGet(PROBE_INCREMENT);
//probe不能等于0
int probe = (p == 0) ? 1 : p; // skip 0
//产生随机数种子
long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
Thread t = Thread.currentThread();
// 将probe和seed设置到当前线程的字段中
UNSAFE.putLong(t, SEED, seed);
UNSAFE.putInt(t, PROBE, probe);
}
其中probeGenerator
和seeder
都是一个AtomicInteger
类型,通过addAndGet
一个固定的偏移量为每个线程产生不同的字段取值(seed还要再经过混淆)。
可以看到,实际上,ThreadLocalRandom是让各个线程持有了自己的随机数种子,这点和ThreadLocal很像。
然后再看一下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;
}
通过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;
}
其他逻辑和Random基本一样。通过以上分析可以看出,线程通过ThreadLocalRandom.current()
获取的是同一个ThreadLocalRandom
对象,但是因为各个线程内部维护的随机数种子不一样(第一次调用current会触发初始化),所以产生的随机数也会不一样。
原创文章,作者:彭晨涛,如若转载,请注明出处:https://www.codetool.top/article/random%e5%9c%a8%e5%a4%9a%e7%ba%bf%e7%a8%8b%e4%b8%8b%e7%9a%84%e9%97%ae%e9%a2%98%e4%bb%a5%e5%8f%8athreadlocalrandom%e7%b1%bb%e5%88%86%e6%9e%90/