我有一个简单的应用程序,模拟资金从一个帐户转移到另一个帐户。我想写一个测试来证明它不是线程安全的。
有一种可能性,线程将以这样的方式运行,即传输将被执行两次。双线程场景:
acc1=1000$
acc2=0$
转移600$
t1:获取acc1余额(1000)
t2:获取acc1余额(1000)
t1:从account1(400)中减去600美元
t2:从account2(400)中减去600美元
t1:将acc2增加600$(600)
t2:增加acc2 600$(1200)
目前我的应用程序不支持多线程,它应该失败,这是我现在的罚款。我可以用调试器模拟错误。然而,当我做线程测试时,它总是成功的。我尝试了不同数量的线程,睡眠,可调用任务
@Test
public void testTransfer() throws AccountNotFoundException, NotEnoughMoneyException, InterruptedException {
Callable<Boolean> callableTask = () -> {
try {
moneyTransferService.transferMoney(ACCOUNT_NO_1, ACCOUNT_NO_2, TRANSFER_AMOUNT);
return true;
} catch (AccountNotFoundException | NotEnoughMoneyException e) {
e.printStackTrace();
return false;
}
};
List<Callable<Boolean>> callableTasks = new ArrayList<>();
int transferTries = 2;
for(int i = 0; i <= transferTries; i++) {
callableTasks.add(callableTask);
}
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.invokeAll(callableTasks);
Assert.assertEquals(ACCOUNT_BALANCE_1.subtract(TRANSFER_AMOUNT), accountRepository.getByAccountNumber(ACCOUNT_NO_1).get().getBalance());
Assert.assertEquals(ACCOUNT_BALANCE_2.add(TRANSFER_AMOUNT), accountRepository.getByAccountNumber(ACCOUNT_NO_2).get().getBalance());
}
以下是汇款代码:
public void transferMoney(String accountFrom, String accountTo, BigDecimal amount) throws AccountNotFoundException, NotEnoughMoneyException {
Account fromAccount = getAccountByNumber(accountFrom);
Account toAccount = getAccountByNumber(accountTo);
if (isBalanceSufficient(amount, fromAccount)) {
//TODO this should be thread safe and transactional
BigDecimal fromNewAmount = fromAccount.getBalance().subtract(amount);
fromAccount.setBalance(fromNewAmount);
// it's possible to fail junits with sleep but I dont want it in code obviously
// Random random = new Random();
// try {
// Thread.sleep(random.nextInt(100));
// } catch (InterruptedException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
BigDecimal toNewAmount = toAccount.getBalance().add(amount);
toAccount.setBalance(toNewAmount);
} else {
throw new NotEnoughMoneyException("Balance on account: " + fromAccount.getNumber() + " is not sufficient to transfer: " + amount);//TODO add currency
}
}
4条答案
按热度按时间insrf1ej1#
如果只需要引入超时而不是模拟
getAccountByNumber
在测试中,例如使用mockito和make mockAccount.setBalance()
花x秒。您可以查看用于编写并发测试的jcstress框架。举个例子。
java并发压力测试(javaconcurrency stress tests,jcstress)是一个实验工具和一套测试,用于帮助研究jvm、类库和硬件中并发支持的正确性。
643ylb082#
你可能会存根
getAccountByNumber(accountFrom)
并在getBalance()
只为你的测试编写代码。无论如何,我建议您将服务代码移动到业务对象,即add方法
Account#transferTo(Account target, BigDecimal amount)
.然后您可以简单地将此方法标记为
synchronized
或者做一些专门的同步。更进一步,你可以创造一个
public synchronized void accept(BigDecimal amount)
,也是。从而标志着每一种换平衡方法的同步。n53p2ov03#
我使用了threadtester,它是我完成任务的绝佳工具:
xpszyzbs4#
欢迎来到多线程的奇妙世界。正如评论所指出的,如果没有完整的源代码,就很难确定证明任何东西的方法。
但是也很难引发线程错误。多线程的第一条规则是,你不能通过练习(例如单元)测试来证明(或者很容易否定)代码是线程安全的。
不安全代码可以执行十亿次而不会出错。在某些平台上,它实际上可能是线程安全的,但在另一些平台上,它始终失败。当您开始使用线程时,所有java代码在所有平台上的行为都是相同的想法就消失了。
这段代码(类的内容)在java中不能保证线程安全:
但它也是一段很小的代码,在某些平台上可能是安全的,或者运行速度很快,可以无误地运行数十亿次。
在大多数平台上都有很好的失败机会。但那又怎样?它并没有证明任何关于原始代码行的东西,有时引入延迟会掩盖问题,特别是在其他地方。
验证多线程代码或使其无效的唯一方法是静态分析和良好的语言知识保证。
您可以尝试在高负载下运行(比如说)两倍于您的平台实际并行运行的线程,并对底层代码进行一些猜测—您很有可能引发问题。但有些错误可能只发生在低负载或任何负载之间。
请记住,如果您修改代码重新测试,它的工作,你已经证明了什么。我不是说你不应该做这样的测试作为最后的检查。
但是千万不要想象单元测试可以帮助证明多线程的可靠性,就像它们帮助单线程一样。这尤其是因为不同的平台可能具有不同的配置(例如进程数、缓存级别、核心数)并经历不同的负载级别。