- 垃圾回收
- 垃圾回收操作不可预测
- 解决方法:
- 测试运行时,GC一次都不执行
- 测试运行时,GC多次运行,需要足够长的时间(几分钟)[更好]
- 动态编译
- JVM 是解释执行和编译执行相结合的。某个类第一次执行时,JVM 会通过解释字节码的方式执行它,在某个时刻,如果一个方法运行的次数足够多,动态编译器会将它编译为机器码,这样代码便由解释执行变为了编译执行(因为编译执行更快)。除此之外,代码还有可能被反编译和重新编译,并且编译的过程也是要消耗时间的。
- 解决方法:
- 运行足够长的时间,使得编译时间和解释执行的时间可以忽略
- 预先运行一段时间代码再开始测试,保证在测试前,代码已被完全编译
- 运行程序时使用命令行选项:-xx: +PrintCompilation,会在动态编译运行时输出一条信息,可以通过这条消息验证动态编译是在测试运行前执行的
- 无用代码消除
- 在 -server 模式下会比 -client 模式下运行的更好,因为 -server 模式更易于通过优化消除无用代码,如果我们要测试的代码被当成无用代码被消除了,测试就没有意义了
- 解决方法:计算某个对象中域的散列值,并将它与
System.nanoTime()
进行比较
if (foo.x.hashCode() == System.nanoTime()) System.out.print(" ");
- 对代码路径的不真实采样
- 运行时,编译器会根据收集到的信息对已编译的代码进行优化,意味着在编译某个程序的方法M时生成的代码,可能与编译另一个程序中的方法M生成的代码不同,从而产生测试的差距。
- 不真实的竞争程度
- 维护两个
AtomicInteger
:putSum
和 takeSum
- 把数放入队列,并加到
putSum
- 把数从队列中取出,并加到
takeSum
- 检查:
putSum == takeSum
- 通过
CyclicBarrier
保证线程的并发
public class PutTakeTest extends TestCase {
protected static final ExecutorService pool = Executors.newCachedThreadPool();
protected CyclicBarrier barrier;
protected final SemaphoreBoundedBuffer<Integer> bb;
protected final int nTrials, nPairs;
protected final AtomicInteger putSum = new AtomicInteger(0);
protected final AtomicInteger takeSum = new AtomicInteger(0);
public static void main(String[] args) throws Exception {
new PutTakeTest(10, 10, 100000).test(); // sample parameters
pool.shutdown();
}
public PutTakeTest(int capacity, int npairs, int ntrials) {
this.bb = new SemaphoreBoundedBuffer<Integer>(capacity);
this.nTrials = ntrials;
this.nPairs = npairs;
// 使用CyclicBarrier保证生产者和消费者线程的并发执行
this.barrier = new CyclicBarrier(npairs * 2 + 1);
}
void test() {
try {
for (int i = 0; i < nPairs; i++) {
pool.execute(new Producer());
pool.execute(new Consumer());
}
barrier.await(); // 阻塞主线程,等待所有线程就绪
barrier.await(); // 等待所有线程执行完成,
// 即生产者和消费者线程中的第二个await执行完
// 相当于循环使用了两次CyclicBarrier
assertEquals(putSum.get(), takeSum.get());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 测试用的随机数生成代码
// 使用方法:通过前一个seed生成后一个seed
// int seed = (this.hashCode() ^ (int) System.nanoTime());
// seed = xorShift(seed);
static int xorShift(int y) {
y ^= (y << 6);
y ^= (y >>> 21);
y ^= (y << 7);
return y;
}
class Producer implements Runnable {
public void run() {
try {
int seed = (this.hashCode() ^ (int) System.nanoTime());
int sum = 0;
barrier.await(); // 通知主线程生产者线程开始执行准备就绪
for (int i = nTrials; i > 0; --i) {
bb.put(seed);
sum += seed;
seed = xorShift(seed);
}
putSum.getAndAdd(sum);
barrier.await(); // 通知主线程生产者线程执行结束
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
class Consumer implements Runnable {
public void run() {
try {
barrier.await(); // 通知主线程消费者线程开始执行准备就绪
int sum = 0;
for (int i = nTrials; i > 0; --i) {
sum += bb.take();
}
takeSum.getAndAdd(sum);
barrier.await(); // 通知主线程消费者线程执行结束
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
LinkedBlockingQueue
的可伸缩性要优于 ArrayBlockingQueue
。
- 违背常理:因为链表元素在插入元素时要分配一个链表节点对象,理论上应该更费时间才对。
- 原因:与基于数组的队列相比,链表队列的 put 和 take 方法支持更高的访问,因为一些优化后的链表队列能将队列的头节点的更新操作和尾节点的更新操作分离开来。