线程同步,线程不同步
测试线程非常困难,这使得为被测多线程系统编写良好的集成测试非常困难。 这是因为在JUnit中,测试代码,被测对象和任何线程之间没有内置的同步。 这意味着,当您必须为创建并运行线程的方法编写测试时,通常会出现问题。 该领域中最常见的场景之一是调用被测方法,该方法在返回之前启动新线程的运行。 在将来某个时刻完成线程的工作时,您需要断言一切都很好。 这种情况的示例可能包括异步地从套接字读取数据或对数据库执行冗长而复杂的一组操作。
例如,下面的ThreadWrapper
类包含一个公共方法: doWork()
。 调用doWork()
会使情况doWork()
并且在将来某个时候,由JVM决定,一个线程会运行,将数据添加到数据库中。
public class ThreadWrapper {
/**
* Start the thread running so that it does some work.
*/
public void doWork() {
Thread thread = new Thread() {
/**
* Run method adding data to a fictitious database
*/
@Override
public void run() {
System.out.println("Start of the thread");
addDataToDB();
System.out.println("End of the thread method");
}
private void addDataToDB() {
// Dummy Code...
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread.start();
System.out.println("Off and running...");
}
}
此代码的直接测试是调用doWork()
方法,然后在数据库中检查结果。 问题是,由于使用了线程,被测对象,测试对象与线程之间没有协调。 编写此类测试时,实现某种协调的一种常见方法是在被测方法的调用与检查数据库中的结果之间放置某种延迟,如下所示:
public class ThreadWrapperTest {
@Test
public void testDoWork() throws InterruptedException {
ThreadWrapper instance = new ThreadWrapper();
instance.doWork();
Thread.sleep(10000);
boolean result = getResultFromDatabase();
assertTrue(result);
}
/**
* Dummy database method - just return true
*/
private boolean getResultFromDatabase() {
return true;
}
}
在上面的代码中,两个方法调用之间有一个简单的Thread.sleep(10000)
。 这种技术的优点是简单易行。 但是它也非常危险。 这是因为它在测试和工作线程之间引入了竞争条件,因为JVM无法保证线程何时运行。 通常,它只能在开发人员的计算机上工作,而在构建计算机上始终失败。 即使可以在构建机器上运行,它也会从表面上延长测试的持续时间; 请记住,快速构建很重要。 正确执行此操作的唯一肯定方法是同步两个不同的线程,而执行此操作的一种技术是将一个简单的CountDownLatch
注入到被测实例中。 在下面的示例中,我修改了ThreadWrapper
类的doWork()方法,将CountDownLatch
添加为参数。
public class ThreadWrapper {
/**
* Start the thread running so that it does some work.
*/
public void doWork(final CountDownLatch latch) {
Thread thread = new Thread() {
/**
* Run method adding data to a fictitious database
*/
@Override
public void run() {
System.out.println("Start of the thread");
addDataToDB();
System.out.println("End of the thread method");
countDown();
}
private void addDataToDB() {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void countDown() {
if (isNotNull(latch)) {
latch.countDown();
}
}
private boolean isNotNull(Object obj) {
return latch != null;
}
};
thread.start();
System.out.println("Off and running...");
}
}
Javadoc API将倒数锁存器描述为:同步辅助,它允许一个或多个线程等待,直到在其他线程中执行的一组操作完成为止。 使用给定的计数初始化CountDownLatch。 由于对countDown()方法的调用,当前的await方法将阻塞,直到当前计数达到零为止,此后,所有等待线程都将被释放,并且所有随后的await调用将立即返回。 这是一种一次性现象,无法重置计数。 如果需要用于重置计数的版本,请考虑使用CyclicBarrier。
CountDownLatch是一种多功能的同步工具,可以用于多种用途。 以1计数初始化的CountDownLatch用作简单的开/关闩锁或门:所有调用await的线程在门处等待,直到被调用countDown()的线程打开为止。 初始化为N的CountDownLatch可以用于使一个线程等待,直到N个线程完成某项操作或某项操作已完成N次。 CountDownLatch的一个有用属性是,它不需要调用countDown的线程在继续进行操作之前就无需等待计数达到零,它只是防止任何线程经过等待状态,直到所有线程都可以通过。
这里的想法是,测试代码将永远不会检查数据库的结果,直到工作线程的run()
方法调用latch.countdown()
。 这是因为测试代码线程阻塞了对latch.await()
的调用。 闩锁latch.countdown()
减少闩锁的计数,并且一旦它为零,阻塞调用闩锁latch.await()
将返回并且测试代码将继续执行,这是安全的, latch.await()
是应知道数据库中应有任何结果。 然后,测试可以检索这些结果并做出有效的断言。 显然,以上代码仅伪造了数据库连接和操作。 问题是您可能不想或不需要直接将CountDownLatch
插入代码中。 毕竟它没有在生产中使用,而且看起来也不是特别干净或优雅。 解决此问题的一种快速方法是简单地将doWork(CountDownLatch latch)
方法包设为私有,并通过公共doWork()
方法公开它。
public class ThreadWrapper {
/**
* Start the thread running so that it does some work.
*/
public void doWork() {
doWork(null);
}
@VisibleForTesting
void doWork(final CountDownLatch latch) {
Thread thread = new Thread() {
/**
* Run method adding data to a fictitious database
*/
@Override
public void run() {
System.out.println("Start of the thread");
addDataToDB();
System.out.println("End of the thread method");
countDown();
}
private void addDataToDB() {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void countDown() {
if (isNotNull(latch)) {
latch.countDown();
}
}
private boolean isNotNull(Object obj) {
return latch != null;
}
};
thread.start();
System.out.println("Off and running...");
}
}
上面的代码使用Google的Guava @VisibleForTesting
批注来告诉我们,出于测试目的,已经稍微放松了doWork(CountDownLatch latch)
方法的可见性。
现在,我意识到,将一个方法调用包私有化以用于测试目的是非常有争议的; 有些人讨厌这个主意,而另一些人则无所不在。 我可以就这个主题写一个整个博客(可能一天),但是对我来说,在别无选择的情况下(例如,当您为遗留代码编写特性测试时)应谨慎使用。 如果可能,应避免使用它,但决不能排除。 毕竟,经过测试的代码比未经测试的代码更好。
考虑到这一点, ThreadWrapper
的下一次迭代将设计出标记为@VisibleForTesting
的方法,以及将CountDownLatch
注入生产代码的需求。 这里的想法是使用策略模式并将Runnable
实现与Thread
分开。 因此,我们有一个非常简单的ThreadWrapper
public class ThreadWrapper {
/**
* Start the thread running so that it does some work.
*/
public void doWork(Runnable job) {
Thread thread = new Thread(job);
thread.start();
System.out.println("Off and running...");
}
}
和一个单独的工作:
public class DatabaseJob implements Runnable {
/**
* Run method adding data to a fictitious database
*/
@Override
public void run() {
System.out.println("Start of the thread");
addDataToDB();
System.out.println("End of the thread method");
}
private void addDataToDB() {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
您会注意到DatabaseJob
类不使用CountDownLatch
。 如何同步? 答案就在下面的测试代码中……
public class ThreadWrapperTest {
@Test
public void testDoWork() throws InterruptedException {
ThreadWrapper instance = new ThreadWrapper();
CountDownLatch latch = new CountDownLatch(1);
DatabaseJobTester tester = new DatabaseJobTester(latch);
instance.doWork(tester);
latch.await();
boolean result = getResultFromDatabase();
assertTrue(result);
}
/**
* Dummy database method - just return true
*/
private boolean getResultFromDatabase() {
return true;
}
private class DatabaseJobTester extends DatabaseJob {
private final CountDownLatch latch;
public DatabaseJobTester(CountDownLatch latch) {
super();
this.latch = latch;
}
@Override
public void run() {
super.run();
latch.countDown();
}
}
}
上面的测试代码包含一个内部类DatabaseJobTester
,该类扩展了DatabaseJob
。 在此类中,在通过调用super.run()
更新了我们的虚假数据库之后,将run()
方法重写为包括对latch.countDown()
的调用。 之所以doWork(Runnable job)
,是因为测试将DatabaseJobTester
实例传递给doWork(Runnable job)
方法,并添加了所需的线程测试功能。 我曾在我的一篇有关测试技术的博客中提到过将被测试对象分类的想法,这是一种非常强大的技术。
因此,得出以下结论:
- 测试线程很难。
- 测试匿名内部类几乎是不可能的。
- 使用
Thead.sleep(...)
是一个冒险的想法,应避免使用。 - 您可以使用策略模式来重构这些问题。
- 编程是做出正确决策的艺术
…放松测试方法的可视性可能是一个好主意,也许不是一个好主意,但稍后会更多……
上面的代码可在unit-testing-threads项目下的队长调试存储库(git://github.com/roghughe/captaindebug.git)中的Github上找到。
参考: Captain Debug的Blog博客上的JCG合作伙伴 Roger Hughes的同步多线程集成测试 。
翻译自: https://www.javacodegeeks.com/2013/02/synchronising-multithreaded-integration-tests.html
线程同步,线程不同步