Java多线程——并发程序的测试

并发程序的测试

相比于串行程序,并发程序潜在的错误发生是随机的,并发测试分为

  • 安全性测试:不发生任何错误的行为,通常测试不变性条件,即判断某个类的行为是否与其规范保持一致
  • 活跃性测试:某个良好的行为终究会发生,包括进展测试和无进展测试
  • 性能测试:吞吐量、响应性、可伸缩性

正确性测试

如下实现一个固定长度的队列,通过计数信号量实现可阻塞的put和take

  • availableitems表示可以从缓存中删除的元素
  • availableSpaces表示可以插入缓存的元素
class BoundedBuffer<E> {
    private final Semaphore availableitems, availableSpaces;
    private final E[] items;
    private int putPosition = 0, takePosition = 0;

    public BoundedBuffer(int capacity) {
        availableitems = new Semaphore(0);
        availableSpaces = new Semaphore(capacity);
        items = (E[]) new Object[capacity];
    }

    public boolean isEmpty() {
        return availableitems.availablePermits() == 0;
    }

    public boolean isFull() {
        return availableSpaces.availablePermits() == 0;
    }

    public void put(E x) throws InterruptedException {
        availableSpaces.acquire();
        doinsert(x);
        availableitems.release();
    }

    public E take() throws InterruptedException {
        availableitems.acquire();
        E item = doExtract();
        availableSpaces.release();
        return item;
    }

    private synchronized void doinsert(E x) {
        int i = putPosition;
        items[i] = x;
        putPosition = (++i == items.length) ? 0 : i;
    }

    private synchronized E doExtract() {
        int i = takePosition;
        E x = items[i];
        items[i] = null;
        takePosition = (++i == items.length) ? 0 : i;
        return x;
    }
}

首先对其进行基本单元测试

@Test
public void testIsEmptyWhenConstruct() {
    BoundedBuffer<Integer> bb = new BoundedBuffer<>(10);
    assertTrue(bb.isEmpty());
    assertFalse(bb.isFull());
}

@Test
public void testIsFullAfterPuts() throws InterruptedException {
    BoundedBuffer<Integer> bb = new BoundedBuffer<>(10);
    for (int i = 0; i < 10; i++) {
        bb.put(i);
    }
    assertTrue(bb.isFull());
    assertFalse(bb.isEmpty());
}

当测试阻塞操作时,需要在线程中启动阻塞操作,等到线程阻塞后再中断它,以此说明阻塞成功,如下子线程调用take从空缓存中获取元素会导致阻塞(若未阻塞则说明测试失败),随后中断并调用join回到主线程判断子线程是否存活

@Test
public void testTakeBlockWhenEmpty() {
    BoundedBuffer<Integer> bb = new BoundedBuffer<>(10);
    Thread taker = new Thread() {
        public void run() {
            try {
                int unused = bb.take();
                fail();
            } catch (InterruptedException success) {
            }
        }
    };
    try {
        taker.start();
        Thread.sleep(2000);
        taker.interrupt();
        taker.join(2000);
        assertFalse(taker.isAlive());
    } catch (Exception e) {
        fail();
    }
}

不能使用Thread.getState来获取线程状态,阻塞线程可能不需要进入WAITING状态,而是自旋等待,而且切换状态需要时间,其获取的值并不准确,只能用于调试信息

安全性测试

想要测试并发类再并发访问的正确执行,需要使用多个线程执行并发操作,但测试程序本身就是并发

测试生产者-消费者模式有几种方式

  • 加入影子列表,操作队列时并同时操作影子列表,然后测试完后判断影子列表的数据是否符合要求
  • 通过对顺序敏感的校验和计算函数计算所有入列、出列元素校验和(校验和应该是随机的,且生成过程不能是同步的),并进行比较,多生产者-消费者模式应该使用顺序不敏感的校验和
public class PutTakeTest {
    private final ExecutorService pool = Executors.newCachedThreadPool();
    public final AtomicInteger putSum = new AtomicInteger(0);
    public final AtomicInteger takeSum = new AtomicInteger(0);

    private final CyclicBarrier barrier;
    private final BoundedBuffer<Integer> bb;
    private final int nTrials, nPairs;

    PutTakeTest(int capacity, int npairs, int ntrials) {
        this.bb = new BoundedBuffer<Integer>(capacity);
        this.nTrials = ntrials;
        this.nPairs = npairs;
        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();//等所有线程执行完成
            pool.shutdown();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    class Producer implements Runnable {
        @Override
        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 {
        @Override
        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);
            }
        }
    }

    private int xorShift(int y) {
        y ^= (y << 6);
        y ^= (y >>> 21);
        y ^= (y << 7);
        return y;
    }
}
@Test
public void main() {
    PutTakeTest putTakeTest = new PutTakeTest(10, 10, 100000);
    putTakeTest.test();
    assertEquals(putTakeTest.putSum.get(),putTakeTest.takeSum.get());
}

对于如上程序

  • xorShift()利用hashCode和nanoTime生成随机数
  • CyclicBarrier数量为2n+1,当n个生产者和n个消费者线程同时运行到barrier.await()时,开启栅栏,从而产生更多的并发操作
  • 栅栏再次启用,等待生产者和消费者线程处理完毕后对比putSum和takeSum是否一致,从而判断BoundedBuffer的并发功能是否正常

通过回调测试

public class TestThreadFactory implements ThreadFactory {

    public final AtomicInteger numCreated = new AtomicInteger();

    private final ThreadFactory mThreadFactory = Executors.defaultThreadFactory();

    @Override
    public Thread newThread(Runnable r) {
        numCreated.incrementAndGet();
        return mThreadFactory.newThread(r);
    }
}

如上,创建线程工厂记录线程数量,测试是否和线程的最大数量相等

@Test
public void testPoolExpansion() throws InterruptedException {
    int MAX_SIZE = 10;
    TestThreadFactory testThreadFactory = new TestThreadFactory();
    ExecutorService executorService = Executors.newFixedThreadPool(MAX_SIZE, testThreadFactory);
    for (int i = 0; i < 10 * MAX_SIZE; i++) {
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });
    }
    for (int i = 0; i < 20 && testThreadFactory.numCreated.get() < MAX_SIZE; i++) {
        Thread.sleep(100);
    }
    assertEquals(testThreadFactory.numCreated.get(), MAX_SIZE);
    executorService.shutdownNow();
}

性能测试

如下新增一个计时的Runnable,并传入CyclicBarrier

public class PutTakeTest {
    private final ExecutorService pool = Executors.newCachedThreadPool();
    public final AtomicInteger putSum = new AtomicInteger(0);
    public final AtomicInteger takeSum = new AtomicInteger(0);

    private final CyclicBarrier barrier;
    private final BoundedBuffer<Integer> bb;
    private final int nTrials, nPairs;
    private final BarrierTimer timer;


    class BarrierTimer implements Runnable {
        private boolean started;
        private long startTime, endTime;

        @Override
        public synchronized void run() {
            long t = System.nanoTime();
            if (!started) {
                started = true;
                startTime = t;
            } else {
                endTime = t;
            }
        }

        public synchronized void clear() {
            started = false;
        }

        public synchronized long getTime() {
            return endTime - startTime;
        }
    }

    PutTakeTest(int capacity, int npairs, int ntrials) {
        this.bb = new BoundedBuffer<Integer>(capacity);
        this.nTrials = ntrials;
        this.nPairs = npairs;
        this.timer = new BarrierTimer();
        this.barrier = new CyclicBarrier(npairs * 2 + 1, timer);
    }

    void test() {
        try {
            timer.clear();
            for (int i = 0; i < nPairs; i++) {
                pool.execute(new Producer());
                pool.execute(new Consumer());
            }
            barrier.await();//等所有线程就绪
            barrier.await();//等所有线程执行完成
            long nsPerItem = timer.getTime() / (nPairs * (long) nTrials);
            System.out.println(" time: " + nsPerItem + " ns/item");
            pool.shutdown();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    class Producer implements Runnable {
        @Override
        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 {
        @Override
        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);
            }
        }
    }

    private int xorShift(int y) {
        y ^= (y << 6);
        y ^= (y >>> 21);
        y ^= (y << 7);
        return y;
    }
}

分别记录容量在1/10/100/1000时,线程数量在1/2/4/8/16/32/64/128时,计算次数为100000时,并发完成计算的性能

@Test
public void main() throws Exception {
    int tpt = 100000;
    for (int cap = 1; cap <= 1000; cap *= 10) {
        System.out.println("cap = " + cap);
        for (int pairs = 1; pairs <= 128; pairs *= 2) {
            System.out.print("pairs = " + pairs);
            PutTakeTest putTakeTest = new PutTakeTest(cap, pairs, tpt);
            putTakeTest.test();
            System.out.println("\t");

        }
    }
}

避免性能测试的陷阱

垃圾回收

垃圾回收可能在任何时刻执行导致运行时间差异,可采取的方法有

  • 确保测试时不执行垃圾回收,可通过-verbose:gc判断是否执行了垃圾回收
  • 确保垃圾回收在测试时执行多次,这个更贴近实际情况

动态编译

执行多次的代码会被编译成机器代码,从解释执行变成直接执行,会对运行时间产生影响,可采取的方法有

  • 运行足够长的时间
  • 预先运行一段时间,等代码完全编译后再开始测试,可通过-xx:+PrintCompilation判断动态编译是否完成
  • 在同一个JVM运行多次,丢弃第一次测试结果(可能发生编译)

不真实的竞争程度

并发程序通过需要执行的操作有

  • 访问共享数据
  • 执行线程本地计算

它们是可以交替运行的,若本地计算时间越长,访问共享数据的开销就会显得越小,故在测试时应该模拟真实运行场景下的计算量

无用代码消除

测试程序通常不会执行任何计算,故容易被JVM消除,如上面PutTakeTest中计算的校验和最终没有使用的话,代码可能被消除

为避免代码消除,可以计算某个对象的hashcode并于任意数值比较

同时计算结果应该是不可预测的,否则编译器可能使用预先计算的结果代替计算过程

其他测试方法

  • 代码审查
  • 静态分析工具
  • 分析与监测工具
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值