Random 类专门用于生成一个伪随机数,它有两个构造器:一个构造器使用默认的种子(以当前时间作为种子),另一个构造器需要程序员显式传入一个long型整数的种子。
ThreadLocalRandom 类是java7新增的一个类,它是Ramdom的增项版。在并发访问的环境下,使用ThreadLocalRandom 来代替Random可以减少线程资源竞争,最终保证系统具有更好的线程安全性。
Random 的使用
Random类位于java.util包下,是一种伪随机,使用非常简单:
public class RandomTest {
public static void main(String[] args) {
Random random = new Random();
for (int i=0;i<10;i++){
System.out.println(random.nextInt());
}
}
}
Random 十分关键的是Seed,核心方法就是利用 AtomicLong +CAS(乐观锁)来更新种子,更新种子的公式是 nextseed = (oldseed * multiplier + addend) & mask;
每个Random实例里面都会维护一个原子性的种子变量来生成随机数,当要生成新的随机数的时候要根据当前种子计算新的种子并更新到原来的原子变量。多线程的环境下使用单个Random实例生成随机数时候,多个线程同时计算随机数的种子会竞争同一个原子变量的更新操作。由于原子变量的更新是CAS操作,同时只能有一个操作能成功,所以就会造成大量线程进行自旋重试,大大的降低性能。
我们看下Random源代码:
在Random类中,有一个AtomicLong的域,用来保存随机种子。其中每次生成的随机数都会根据随机种子做移位操作得到新的随机数:
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在大量竞争时,由于很多CAS操作会造成失败,不选的自旋,造成CPU开销比较大而且吞吐量也会下降。
ThreadLocalRandom的使用
ThreadLocalRandom 是Random的子类,它是将Seed 随机种子隔离到当前线程的随机数生成器,从而解决了Random 多线程上竞争种子的问题,它的思想本质和ThreadLocal一样。
ThreadLocalRandom.current().nextX(…)}
基本原理:
current() 的时候初始化一个一个初始种子到线程,每次nextSeed 再使用之前的种子生成新的种子:
public static ThreadLocalRandom current() {
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
上述使用了单例模式,如果当前线程中如果没有绑定 seed,则进行初始化,然后返回 当前ThreadLocalRandom实例。
static final ThreadLocalRandom instance = new ThreadLocalRandom();
也就是说,instance是一个公共的 ThreadLocalRandom ,并且只有一个,只不过seed是每个线程一份。
那么单例模式下随机种子又是怎样隔离的呢?
我们继续看 UNSAFE.getInt(Thread.currentThread(), PROBE) 方法
我们先看 PROBE
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);
}
}
threadLocalRandomProbe 是线程中的一个变量,用来表示ThreadLocalRandom 是否进行了初始化,如果是非0,表示已经初始化,等于0表示还未进行初始化,如果等于0 ,会执行 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);
}
当Thread对象被创建后,threadLocalRandomProbe 和 threadLocalRandomSeed 都为0,当这个线程首次调用 current 方法时,threadLocalRandomProbe 为0 会调用localinit方法,方法中会初始化 threadLocalRandomSeed,并将threadLocalRandomProbe更新为非0,表示已经初始化。
当localinit()执行完成后,就会返回ThreadLocalRandom的单例,然后直接可以调用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;
}
如上代码首先使用 r = UNSAFE.getLong(t, SEED)获取当前线程中threadLocalRandomSeed变量的值,然后在种子的基础上累加GAMMA值作为新种子,然后使用UNSAFE的putLong方法把新种子放入当前线程的threadLocalRandomSeed变量。
ThreadLocalRandom 的错误用法
把ThreadLocalRandom的实例设置到静态变量中,在多线程中重用:
我们看下面的例子:
public class ThreadLocalRandomTest {
private static ThreadLocalRandom random = ThreadLocalRandom.current();
public static void main(String[] args) {
Executor executor = Executors.newCachedThreadPool();
for(int i=0;i<100;i++){
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread()+":"+ random.nextInt(100));
}
});
}
}
}
测试结果:
Thread[pool-1-thread-3,5,main]:4
Thread[pool-1-thread-1,5,main]:4
Thread[pool-1-thread-2,5,main]:4
Thread[pool-1-thread-7,5,main]:4
Thread[pool-1-thread-5,5,main]:4
Thread[pool-1-thread-4,5,main]:4
Thread[pool-1-thread-6,5,main]:4
再多线程环境下回出现重复数据。
从上面的源码中可以读出,初始化值seed值是绑定到主线程上的,而当其他线程调用nextSeed() 方法时,虽然非主线程和seed的键值对之前并没有存入到UNSAFEz中,但是我们却从UNSAFE里获取到了非主线程的seed值,虽然我不知道取出来的 seed 到底是什么,但肯定不是多线程下想要的结果,而这也导致了多线程下产生的随机数是重复的。
我们修改为 TheadLocalRandom.curent().nextInt() 就不会出现呢重复的了
public class ThreadLocalRandomTest {
public static void main(String[] args) {
Executor executor = Executors.newCachedThreadPool();
for(int i=0;i<100;i++){
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread()+":"+ ThreadLocalRandom.current().nextInt(100));
}
});
}
}
}