Java求职面试线程编程测试。他们想要什么答案?

时间:2017-02-27 13:59:21

标签: java multithreading

编辑: 我添加了下面使用的实现和...

...我看到我做错了什么:AtomicInteger平衡对象 是(我认为)线程安全,涉及两个操作,即获取 当前的余额,然后更新它。所以平衡可能会改变 获取它并更新之间。

尽管如此,我仍然想知道什么是最好的解决方案 他们在寻找什么。

(另外,就可扩展性而言,例如转移的可能性 余额,我故意没有解决它。它似乎超越了 任务范围。我不好,如果不是)

我刚刚在家庭编程测试中得到了第二个否定回复 涉及未来雇主提供的线索。

我不知道我给出的解决方案是否必然是错误的 - 它可能不是 成为他们想要的人。在这一点上,我很困惑,没有 这是什么意思。

这基本上就是你给出的Java问题:

public interface BalanceManagementSystem { //Edit: Changed name 
 /**
  * Deduct 'amountToWithdraw' of the given 'accountId' from account.
  * @param accountId The ID of the account to withdraw from
  * @param amountToWithdraw The quantity to withdraw
  * @return TODO: to be implemented
  */
 WithdrawalResult withdrawFromAccount(String accountId, int amountToWithdraw);

 /**
  * Add 'amountToDeposit' of the given accountId to inventory.
  * @param accountId The ID of the account to deposit to
  * @param amountToDeposit The quantity to deposit
  * @return TODO: to be implemented
  */
 DepositResult depositToAccount(String accountId, int amountToDeposit);
}

...所以你必须使用线程安全的方法实现接口 以上两点。我知道。

不能使用标准JDK包之外的外部依赖项。

其余要求有点模糊。我结束了 没有数据库,使用:

  1. HashMap作为包含(我自己的)帐户的数据结构 对象。
  2. 用于将余额存储在Account对象中的AtomicInteger 强制执行线程安全。
  3. 显然这不是他们想要的答案。我很确定 线程安全问题是出错的地方。

    因此,给予并评估线程测试的雇主或经理 像这样或通过一个的员工,有什么解决方案 要求?

    请注意

    用于存储的HashMap使用accountId作为密钥。

    AtomicInteger不包含subtractAndGet()所以我将其子类化,创建了AtomicIntegerPlus。 subtractAndGet()只是对原生addAndGet()的一个小修改。

    class Account {
        private String accountId;
    
        private AtomicIntegerPlus balance; //Thread-safe object subclassed from AtomicInteger
    
        Account(String acctId, String loc, AtomicIntegerPlus amt) {
            this.accountId = acctId;
            this.balance = amt;
        }
    
        public void addToBalance(int amt) {
            balance.addAndGet(amt);
        }
    
        public void subtractFromBalance(int amt) {
            balance.subtractAndGet(amt);
        }
    }
    
    class BMS implements BalanceManagementSystem {
        private HashMap<String, Account> hashMap;
        -
        -
    
        /**
        * Deduct 'amountToWithdraw' of the given 'accountId' from account.
        * @param accountId The ID of the account to withdraw from
        * @param amountToWithdraw The quantity to withdraw
        * @return withdrawalResult
        */
        public WithdrawalResult withdrawFromAccount(String accountId, int amountToWithdraw) {
    
            if (hashMap.containsKey(accountId)) {           
                Account tmpAccount = (Account)hashMap.get(accountId);           
                int balance = tmpAccount.getBalance();
    
                if (balance >= amountToWithdraw) {              
                    tmpAccount.subtractFromBalance(amountToWithdraw);
                    hashMap.put(tmpAccount.getAccountId(), tmpAccount); //Updatebalance with new amount
    
                    withdrawalResult.setMessage("Withdrawn. You now have " + tmpAccount.getBalance() + " left");
    
                } else {
                    withdrawalResult.setMessage("Don't have the balance for your request. Only " + balance + " left");
                }
    
            } else {
                withdrawalResult.setMessage("Sorry: account id " + accountId + " does not exist");
            }
    
            return withdrawalResult;
        }
    
        /**
        * Add 'amountToDeposit' of the given accountId to inventory.
        * @param accountId The ID of the account to deposit to
        * @param amountToDeposit The quantity to deposit
        * @return depositResult
        */
        public DepositResult depositToAccount(String accountId, int amountToDeposit) {      
    
            if (hashMap.containsKey(accountId)) {           
                Account tmpAccount = (Account)hashMap.get(accountId);
    
                tmpAccount.addToBalance(amountToDeposit);       
                hashMap.put(tmpAccount.getAccountId(), tmpAccount);// Update Balance with new amount
    
                depositResult.setMessage("Deposited. You now have " + tmpAccount.getBalance() + " left");
    
            } else {
                depositResult.setMessage("Sorry: account id " + accountId + " does not exist");
            }
    
            return depositResult;
        }
    }
    

3 个答案:

答案 0 :(得分:3)

假设您在特定帐户中拥有1000。正在发生两次同时取款900次。您的基于AtomicInteger的解决方案是否确保您不会以负数量结束?它可以完成(例如通过比较循环中的compareAndSet),但与同步相比可能略微过于复杂。

根据我的经验,实际问题(可能是您实施的后续跟踪)并不仅仅是以线程安全的方式实施撤销或存款,而是在线程安全的两个帐户之间进行转移办法。您需要确保在并发事务的情况下不会透支帐户,并且如果使用简单的同步方案并最终导致交叉传输,则不会导致死锁。基于AtomicInteger的解决方案使得实现和推理变得相当困难(我仍然会接受它,但你需要100%确定防御CAS逻辑)

答案 1 :(得分:0)

  

HashMap作为包含(我自己的)帐户对象的数据结构。

第一个问题是HashMap不是同步类,您似乎没有保护它免受同时访问。如果2个线程同时尝试访问相同的HashMap,则可能会导致数据损坏,无限循环等。

快速解决方案是使用ConcurrentHashMap来阻止程序崩溃,但不能完全处理代码中的竞争条件。您需要担心的是,如果2个线程将同时对帐户进行第一次操作。如果您总是进行put(...)操作,那么第二个线程将覆盖第一个。您需要使用ConcurrentHashMap&#39; putIfAbsent(...)方法。类似的东西:

Account account = concurrentMap.get(accountId);
if (account == null) {
   account = new Account();
   Account oldAccount = concurrentMap.putIfAbsent(accountId, account);
   if (oldAccount != null) {
      // some other thread beat us to it
      account = oldAccount;
   }
}

我看到的另一个问题是你正在将Account重新放入地图中。这应该只在上面的代码中完成一次。它应该采用您的两种方法共享的getAccount(accountId)方法。

  

...我看到我做错了什么:当AtomicInteger平衡对象(我认为)线程安全时,涉及两个操作,即获取当前余额,然后更新它。因此,在获取和更新之间平衡可能会发生变化。

这也是需要一些技巧来解决的问题。您的任何更新循环都必须类似于以下伪代码:

while (true) {
   int balance = account.getBalance();
   // test balance and print error here
   int newBalance = balance + adjustment;
   if (account.adjustBalance(balance, newBalance)) {
      break;
   }
}

然后在你的Account对象中你会做类似的事情:

 public boolean adjustBalance(int oldBalance, int newBalance) {
    // this returns true if balance is still oldBalance and only then it updates it to new
    return balance.compareAndSet(oldBalance, newBalance);
 }

每当您更新余额时,您需要获取值,更改值,然后尝试将其存储回来,但如果余额未被其他线程更新,则成功同时。这个循环是一个非常典型的模式,用于JDK和其他地方的许多不同的地方。

最后,我认为将AtomicInteger扩展为添加subtractAndGet(...)是过分的。带有负数的AddAndGet(...)会非常好。

答案 2 :(得分:0)

您提供的解决方案显示了一些缺点:

a)您似乎还没有完全理解引用类型(Java中的类)和值类型之间的区别。这由以下代码表示:

Account tmpAccount = (Account)hashMap.get(accountId);
// ...
tmpAccount.subtractFromBalance(amountToWithdraw);
hashMap.put(tmpAccount.getAccountId(), tmpAccount);

在这里,您将从地图中通过引用获取Account类型的对象并更改其状态(使用substractFromBalance)。没有必要将它放回到地图中 - 您已经更新了仍然存储在地图中的参考 - 而不是它的副本。

此错误与

一起发生

b)HashMap周围没有同步

如果帐户在生命周期中发生变化(这对于银行系统来说是很正常的话),那么您需要同步所有对hashMap的访问。你不为此做任何事情。您可能会认为帐户从程序开始就已修复,并且永远不会更改 - 这将允许您忽略同步。但是在你的代码示例中,你可以阻止自己进行这样的论证:通过调用hashMap.put(...),你可以改变hashMap,这是非线程安全的操作。

c)在检查余额和更新余额之间,您的程序中存在竞争条件。如果某些其他线程也从中间退出账户,则最终会出现负余额。如果你选择原子,你需要一个compareExchange循环。

d)可能不正确的原子添加实现

我只能推测这一点,因为你没有提供AtomicIntegerPlus的源代码。我的第一个想法是:你不需要这个,因为正常AtomicInteger已经拥有你需要的一切。而且你的程序也不需要substractAndGet(你永远不会使用返回值)。然后问题是你是如何实现的:如果它像AtomicInteger.addAndGet(-value); return AtomicInteger.get();那样那么它就错了,因为它不再是原子的。无论如何AtomicInteger.addAndGet(-value);已经足够了。

一个简单的正确解决方案只是在HashMap周围使用了一个锁,甚至不需要原子:

public WithdrawalResult withdrawFromAccount(String accountId, int amountToWithdraw) {
    WithdrawalResult withDrawalResult = new WithdrawalResult();

    synchronized(hashMap) {
        Account account = hashMap.get(accountId);    
        if (account != null) {                           
            int balance = account.getBalance();
            if (balance >= amountToWithdraw) {              
                account.setBalance(balance - amountToWithdraw);
                withdrawalResult.setMessage("Withdrawn. You now have " +   tmpAccount.getBalance() + " left");
            } else {
                withdrawalResult.setMessage("Don't have the balance for your request. Only " + balance + " left");
            }
        } else {
            withdrawalResult.setMessage("Sorry: account id " + accountId + " does not exist");
        }
    }

    return withdrawalResult;
}