编辑: 我添加了下面使用的实现和...
...我看到我做错了什么: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包之外的外部依赖项。
其余要求有点模糊。我结束了 没有数据库,使用:
显然这不是他们想要的答案。我很确定 线程安全问题是出错的地方。
因此,给予并评估线程测试的雇主或经理 像这样或通过一个的员工,有什么解决方案 要求?
请注意
用于存储的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;
}
}
答案 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;
}