测试应用程序是否是线程安全的
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了测试应用程序是否是线程安全的相关的知识,希望对你有一定的参考价值。
我有简单的应用程序,模拟从一个帐户到另一个帐户的汇款。我想写一个测试,表明它不是线程安全的。
线程有可能以这样的方式进行,即传输将进行两次。两个线程场景:
- ACC1 = $ 1000个
- acc2 = 0 $
- 转600美元 T1:获得acc1余额(1000) T2:获得acc1余额(1000) T1:从account1减去600 $(400) T2:从account2减去600 $(400) T1:将acc2提高600 $(600) T2:将acc2提高600 $(1200)
目前我的应用程序不支持多线程,它应该失败,现在对我来说没问题。我能够使用调试器模拟错误。但是,当我进行线程测试时,它总是成功的。我试过不同数量的线程,睡眠,callabletasks
@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
欢迎来到多线程的精彩世界。正如评论所指出的那样,如果没有完整的源代码,就很难确定一种证明任何东西的方法。
但也很难引发线程错误。多线程的第一条规则是你不能通过练习(例如单元)测试证明(或容易反驳)代码是线程安全的。
不安全的代码可以执行十亿次而不会出错。在某些平台上,它实际上可能是线程安全的,但在其他平台上则一致地失败。当您开始使用线程时,所有Java代码在所有平台上的行为相同的想法都会失控。
此代码(您的类的发明内容)在Java中不保证是线程安全的:
balance+=transaction;
但它也是如此小的一段代码,它可能在某些平台上是安全的,或者只是如此之快,它运行数十亿次而没有错误。
int temp=balance;
Thread.sleep(1000);
balance=temp+transaction;
很有可能最终在大多数平台上失败。但那又怎么样?它没有证明原始的代码行,有时会引入延迟掩盖问题,尤其是其他地方。
验证或使多线程代码无效的唯一方法是静态分析和对语言保证的良好了解。
您可以尝试以高负载运行(比如)您的平台实际并行运行的线程数量的两倍,并对您很有可能引发问题的底层代码进行一些猜测。但有些错误可能只发生在低负载或其间的任何负载。
请记住,如果您修改代码重新测试并且它工作正常,您就没有证明什么。我不是说你不应该做最后检查这样的测试。
但是,从来没有想过单元测试有助于证明多线程的可靠性,它们有助于单线程。这尤其是因为不同的平台可能具有不同的配置(例如,进程数,高速缓存级别,核心)并且经历不同级别的负载。
如果只需要在测试中引入超时而不是模拟getAccountByNumber
,例如通过使用Mockito并使模拟Account.setBalance()
需要X秒。
您可以查看jcstress框架来编写并发测试。一个例子here。
Java Concurrency Stress测试(jcstress)是一个实验性工具和一套测试,用于帮助研究JVM,类库和硬件中的并发支持的正确性。
您可以将getAccountByNumber(accountFrom)
存根并在getBalance()
代码中实现一些“等待代码”,仅用于您的测试。
无论如何,我建议你将服务代码移动到业务对象,即添加方法Account#transferTo(Account target, BigDecimal amount)
。
然后,您可以将此方法标记为synchronized
或执行一些专用同步。
更进一步,你也可以创建一个public synchronized void accept(BigDecimal amount)
。从而将每个平衡变化方法标记为同步。
我使用过threadtester,这对我的任务很有用:
<dependency>
<groupId>org.mapdb</groupId>
<artifactId>thread-weaver</artifactId>
<version>3.0.mapdb</version>
<scope>test</scope>
</dependency>
以上是关于测试应用程序是否是线程安全的的主要内容,如果未能解决你的问题,请参考以下文章