多线程环境下生成随机数
生成伪随机数据
Java里有伪随机型和安全型两种随机数生成器。伪随机生成器根据特定公式将seed转换成新的伪随机数据的一部分。安全随机生成器在底层依赖到操作系统提供的随机事件来生成数据。
安全随机生成器
- 需要生成加密性强的随机数据的时候才用它;
- 生成速度慢;
- 如果需要生成(Linux /dev/random 就是个这样的安全随机生成器)大量随机数据,可能会产生堵塞需要等待外部中断事件。
而伪随机生成器,只依赖于”seed”的初始值。如果你给生成算法提供相同的seed,可以得到一样的伪随机序列。一般情况下,由于它是计算密集型的(不依赖于任何IO设备),因此生成速度更快。接下来,我们将回顾伪随机生成器的进化史。
java.util.Random
java.util.Random 从Java 1.0开始就存在了。它是一个线程安全类,理论上可以通过它同时在多个线程中获得互不相同的随机数。这样的线程安全是通过AtomicLong实现的。
Random 使用 AtomicLong CAS (compare-and-set)操作来更新它的seed,尽管很多非阻塞式算法中使用了非阻塞式原语,CAS在资源高度竞争时的表现依然糟糕。在后面的测试结果中你可以看到它的糟糕表现。
java.util.concurrent.ThreadLocalRandom
Java 7增加了java.util.concurrent.ThreadLocalRandom 并企图将它与 java.util.Random 结合以克服所有的性能问题。ThreadLocalRandom类继承自java.util.Random。
ThreadLocalRandom 的主要实现细节:
- 它使用一个普通的 long 而不是使用 Random 中的 AtomicLong 作为seed。
- 你不能自己创建ThreadLocalRandom实例,因为它的构造函数没有设置为public。可以使用它的静态工厂ThreadLocalRandom.current(),这个工厂方法调用了内置的ThreadLocal<
ThreadLocalRandom>。 - 它是CPU缓存感知式的,使用8个 long 虚拟域来填充64位L1高速缓存行。
所有这些改变都是很重要的,在接下来的测试中你将会感受到。
测试
我们将进行下面5种测试:
- 一个单独的java.util.Random被N个线程共享
- ThreadLocal< Random>
- java.util.concurrent.ThreadLocalRandom
- java.util.Random[],其中每个线程N使用一个数组下标为N的Random。
- java.util.Random[],其中每个线程N使用一个数组下标为N * 2的Random。
所有的测试都使用了封装在RandomTask类里的方法。每个方案都说明了如何使用随机生成器。
package com.ykp.concurrent;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;
public class ThreadLocalRandomGenerator {
private static final long COUNT = 10000000;
private static final int THREADS = 2;
public static void main(String[] args) {
System.out.println("Shared Random");
testRandom(THREADS, COUNT);
// System.out.println("ThreadLocal<Random>");
// testTL_Random(THREADS, COUNT);
// System.out.println("ThreadLocalRandom");
// testTLRandom(THREADS, COUNT);
// System.out.println("Shared Random[] with no padding");
// testRandomArray(THREADS, COUNT, 1);
// System.out.println("Shared Random[] with padding");
// testRandomArray(THREADS, COUNT, 2);
}
// runner for all tests
private static class RandomTask implements Runnable {
private final Random rnd;
protected final int id;
private final long cnt;
private final CountDownLatch latch;
private RandomTask(Random rnd, int id, long cnt, CountDownLatch latch) {
this.rnd = rnd;
this.id = id;
this.cnt = cnt;
this.latch = latch;
}
protected Random getRandom() {
return rnd;
}
@Override
public void run() {
try {
final Random r = getRandom();
latch.countDown();
latch.await();
final long start = System.currentTimeMillis();
int sum = 0;
for (long j = 0; j < cnt; ++j) {
sum += r.nextInt();
}
final long time = System.currentTimeMillis() - start;
System.out.println("Thread #" + id + " Time = " + time / 1000.0
+ " sec, sum = " + sum);
} catch (InterruptedException e) {
}
}
}
private static void testRandom(final int threads, final long cnt) {
// 可以保证所有线程同时开始执行
final CountDownLatch latch = new CountDownLatch(threads);
final Random r = new Random(100);
for (int i = 0; i < threads; ++i) {
final Thread thread = new Thread(new RandomTask(r, i, cnt, latch));
thread.start();
}
}
private static void testRandomArray(final int threads, final long cnt,
final int padding) {
final CountDownLatch latch = new CountDownLatch(threads);
final Random[] rnd = new Random[threads * padding];
for (int i = 0; i < threads * padding; ++i)
// allocate together
rnd[i] = new Random(100);
for (int i = 0; i < threads; ++i) {
final Thread thread = new Thread(new RandomTask(rnd[i * padding],
i, cnt, latch));
thread.start();
}
}
private static void testTLRandom(final int threads, final long cnt) {
final CountDownLatch latch = new CountDownLatch(threads);
for (int i = 0; i < threads; ++i) {
final Thread thread = new Thread(
new RandomTask(null, i, cnt, latch) {
// 改变RandowTask中获取Random的方式
@Override
protected Random getRandom() {
return ThreadLocalRandom.current();
}
});
thread.start();
}
}
private static void testTL_Random(final int threads, final long cnt) {
final CountDownLatch latch = new CountDownLatch(threads);
final ThreadLocal<Random> rnd = new ThreadLocal<Random>() {
@Override
protected Random initialValue() {
return new Random(100);
}
};
for (int i = 0; i < threads; ++i) {
final Thread thread = new Thread(
new RandomTask(null, i, cnt, latch) {
@Override
protected Random getRandom() {
return rnd.get();
}
});
thread.start();
}
}
}
测试结果
Shared java.util.Random
第一个测试使用的是共享的java.util.Random实例。高争夺的CAS操作严重影响了它的性能。仅仅开两个线程都会受争夺的影响,然后现实中很少会发生这种争夺的情况。下面是所有线程的最小和最大运行时间。
“Shared” java.util.concurrent.ThreadLocalRandom
接下来的测试使用第二个类——java.util.concurrent.ThreadLocalRandom。 如你所见,在程序运行的线程数低于CPU的线程数时性能没有下降,当程序运行的线程数超过CPU的线程数时性能才线性的降低。另一个要注意的重点是,单一线程执行的效率是第一个案例的3倍——无竞争的CAS操作仍然表现糟糕。
“Shared” ThreadLocal《java.util.Random》
把java.util.Random实例装入ThreadLocal后执行的效率有些不一样,当线程数超过CPU核心数时性能就下降了——听起来像是CAS操作不能执行那么多单元。不过接下来的性能下降是线性的,和第二个案例很相似。
Array of java.util.Random
最后我想要检查CPU缓存行对ThreadLocalRandom 的改善作用和模拟java.util.Random在缺乏这种功能下的情况。你需要做的就是创建可以被很多线程使用的java.util.Random实例,我用java.util.Random[]来实现此目的并用array[N]表示第N个线程。
总结
- 任何情况下都不要在多个线程间共享一个java.util.Random实例,而该把它放入ThreadLocal之中。
- Java7在所有情形下都更推荐使用java.util.concurrent.ThreadLocalRandom——它向下兼容已有的代码且运营成本更低。
更多推荐
所有评论(0)