最近工作中遇到了一个需求,需要以一定的概率过滤掉一部分的流量,想想只能用Random了,因为是在多线程环境下,我还特意确认了下Random在多线程是否能正常运行,Random的实现也比较简单,初始化的时候用当前的事件来初始化一个随机数种子,然后每次取值的时候用这个种子与有些MagicNumber运算,并更新种子。最核心的就是这个next的函数,不管你是调用了nextDouble还是nextInt还是nextBoolean,Random底层都是调这个next(int bits)。
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));
}
为了保证多线程下每次生成随机数都是用的不同,next()得保证seed的更新是原子操作,所以用了AtomicLong的compareAndSet(),该方法底层调用了sum.misc.Unsafe的compareAndSwapLong(),也就是大家常听到的CAS, 这是一个native方法,它能保证原子更新一个数。
既然Random满足我的需求,又能在多线程下正常运行,所以我直接用了random,后来在codeReview中,同事提出用concurrent.ThreadLocalRandom来替代Random。我脑子里立马冒出一个问题,既然random是线程安全的,为什么concurrent包里还要实现一个random。在oracle的jdk文档里发现这样一句话
use of ThreadLocalRandom rather than shared Random objects in concurrent programs will typically encounter much less overhead and contention. Use of ThreadLocalRandom is particularly appropriate when multiple tasks (for example, each a ForkJoinTask) use random numbers in parallel in thread pools.
大意就是用ThreadLocalRandom更适合用在多线程下,能大幅减少多线程并行下的性能开销和资源争抢。
既然文档里说的牛逼,到底能有多少的性能提升?我做了一个简单的测试。测试环境:24核 CPU, jdk8,每个随机生成100000个double数,,分别测试不同线程数下rando和ThreadLocalRandom的运行时间,数据如下
ThreadNum,Random,ThreadLocalRandom
50,1192,575
100,4031,162
150,6068,223
200,8093,287
250,10049,248
300,12346,200
350,14429,212
400,16491,62
450,18475,96
500,11311,97
550,12421,90
600,13577,102
650,14718,111
700,15896,127
750,17101,129
800,17907,203
850,19261,226
900,21576,151
950,22206,147
1000,23418,174
ThreadLocalRandom虽然也有波动,但基本上是平的,而random随着线程数的增加一直在增加,在1000个线程时两者居然有百倍的性能差距。不过这里有个让人百思不得其解的现象,为什么random的耗时在500个线程的时候又掉下来,测试多次都是这个情况,可见并不是偶发现象。
我也在本人的笔记本上测了下,我笔记本双核i7,ThreadLocalRandom和Random性能差距最高也有100倍,我发现我笔记本比公司服务器跑的快(数据如下)。。。。我也在一台1核的阿里云ECS上测试了,按道理1核心的技术上,即便是多线程起始也是串行执行的,但ThreadLocalRandom和Random在1000个线程的情况下也有6倍的性能差距。
既然ThreadLocalRandom在多线程下表现这么牛逼,它究竟是如何做到的?我们来看下源码,它的核心代码是这个
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;
}
起始ThreadLocalRandom是对每个线程都设置了单独的随机数种子,这样就不会发生多线程同时更新一个数时产生的资源争抢了,用空间换时间。
最后附上Random和ThreadLocalRandom的性能测试代码
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
public class RandomTest {
private static Random random = new Random();
private static final int N = 100000;
// Random from java.util.concurrent.
private static class TLRandom implements Runnable {
@Override
public void run() {
double x = 0;
for (int i = 0; i < N; i++) {
x += ThreadLocalRandom.current().nextDouble();
}
}
}
// Random from java.util
private static class URandom implements Runnable {
@Override
public void run() {
double x = 0;
for (int i = 0; i < N; i++) {
x += random.nextDouble();
}
}
}
public static void main(String[] args) {
System.out.println("threadNum,Random,ThreadLocalRandom");
for (int threadNum = 50; threadNum <= 2000; threadNum += 50) {
ExecutorService poolR = Executors.newFixedThreadPool(threadNum);
long RStartTime = System.currentTimeMillis();
for (int i = 0; i < threadNum; i++) {
poolR.execute(new URandom());
}
try {
poolR.shutdown();
poolR.awaitTermination(100, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
String str = "" + threadNum +"," + (System.currentTimeMillis() - RStartTime)+",";
ExecutorService poolTLR = Executors.newFixedThreadPool(threadNum);
long TLRStartTime = System.currentTimeMillis();
for (int i = 0; i < threadNum; i++) {
poolTLR.execute(new TLRandom());
}
try {
poolTLR.shutdown();
poolTLR.awaitTermination(100, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(str + (System.currentTimeMillis() - TLRStartTime));
}
}
}