测试应用程序是否线程安全

时间:2019-02-04 11:14:08

标签: java multithreading

我有一个简单的应用程序,可以模拟从一个帐户到另一帐户的资金转移。我想编写一个测试,表明它不是线程安全的。

线程可能会以两次传输完成的方式进入。两线程方案:

  • acc1 = 1000 $
  • acc2 = 0 $
  • 转让600 $

    1. T1:获取acc1余额(1000)
    2. T2:获得acc1余额(1000)
    3. T1:从account1(400)中减去600 $
    4. T2:从account2(400)中减去600 $
    5. T1:将acc2提高600 $(600)
    6. 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 个答案:

答案 0 :(得分:1)

在测试中,如果只需要引入超时而不是模拟getAccountByNumber,例如通过使用Mockito并使模拟Account.setBalance()花费X秒。

您可以看看用于编写并发测试的jcstress框架。一个示例here

  

Java并发压力测试(jcstress)是一种实验工具和一整套测试,可帮助研究JVM,类库和硬件中并发支持的正确性。

答案 1 :(得分:1)

您可能只对测试存根getAccountByNumber(accountFrom)并在getBalance()代码中实现一些“等待代码”。

无论如何,我建议您将服务代码移至业务对象,即添加方法Account#transferTo(Account target, BigDecimal amount)

然后,您可以简单地将此方法标记为synchronized或进行一些专用的同步。

更进一步,您也可以创建一个public synchronized void accept(BigDecimal amount)。从而将每种平衡更改方法标记为同步。

答案 2 :(得分:1)

欢迎来到精彩的多线程世界。正如评论所指出的那样,要在没有完整源代码的情况下确定证明一切的方法将非常困难。

但是也很难引起线程错误。多线程的第一个规则是,您不能通过运动(例如,单元)测试来证明(或轻易地证明)代码是线程安全的。

不安全的代码可能执行十亿次而没有错误。实际上,在某些平台上,它可能是线程安全的,但在其他平台上,则始终失败。当您开始使用线程时,所有Java代码在所有平台上的行为都相同的想法就不再成立了。

不能保证此代码(类的发明内容)在Java中是线程安全的:

balance+=transaction;

但这只是一小段代码,在某些平台上可能是安全的,或者运行速度如此之快,数十亿次而没有错误。

int temp=balance;
Thread.sleep(1000);
balance=temp+transaction;

在大多数平台上最终失败的可能性很大。 那又如何? 它无法证明原始代码行,有时甚至会引入延迟掩盖问题,尤其是在其他地方。

验证或使多线程代码无效的唯一方法是静态分析和对语言保证的充分了解。

您可以尝试在高负载下运行(例如)两倍于平台实际可以并行运行的线程,并对基本代码进行一些猜测,从而很有可能引发问题。但是某些错误可能仅在低负载或介于两者之间的任何负载下才会发生。

请记住,如果您修改了代码重新测试后仍然有效,您什么也没证明。我并不是说您不应该进行最终检查之类的测试。

但是,请不要想象单元测试会以帮助单线程的方式帮助证明多线程的可靠性。尤其是因为不同的平台可能具有不同的配置(例如,进程数,缓存级别,内核)并且承受不同级别的负载。

答案 3 :(得分:0)

我使用了threadtester,它是完成任务的绝佳工具:

DB::table('my_table')->select('id')
    ->where(DB::raw("UCASE(name)"), $upper_name) 
    ->first();