让我们假设我想实现一个非常简单的Bank Account
类,我们想要关注并发和多线程问题,
即使synchronized
是balance
,制作以下方法AtomicInteger
也是个好主意吗?
另一方面,如果我们将所有方法都同步,那么就不再使用AtomicInteger
了,对吧?
import java.util.concurrent.atomic.AtomicInteger;
public class Account {
AtomicInteger balance;
public synchronized int checkBalance(){
return this.balance.intValue();
}
public synchronized void deposit(int amount){
balance.getAndAdd(amount);
}
public synchronized boolean enoughFund(int a){
if (balance.intValue() >= a)
return true;
return false;
}
public synchronized boolean transfer_funds(Account acc, int amount){ // dest : acc
if (enoughFund(amount)){
withdraw(amount);
acc.deposit(amount);
return true;
}
return false;
}
public synchronized boolean withdraw(int amount){
if (checkBalance() < amount)
return false;
balance.getAndAdd(-1 * amount);
return true;
}
}
答案 0 :(得分:7)
两者都是,最好让它同步,不需要Atomic。
如果您仅依靠Atomic而不是同步,则可能会出现此方法中的问题:
if (enoughFund(amount)){
withdraw(amount);
acc.deposit(amount);
return true;
}
因为Atomic只保证您的整数可以安全地从同时访问,这意味着enoughFund(amount)
将保证为amount
提供正确的值,即使它是由其他一些帖子写的。但是,仅Atomic并不能保证在此行获得的值与下一行代码中的值相同,因为另一个线程可以在这两行之间执行另一个Atomic操作,从而导致withdraw(amount);
能够将您的余额设置为零以下。
答案 1 :(得分:5)
将您的金额声明为AtomicInteger
并不会阻止线程在方法执行过程中被抢占(如果它未同步)。因此,例如,如果您的方法transfer_funds
未以任何方式同步,即使您的金额为AtomicInteger
public /* synchronized */ boolean transfer_funds(Account acc, int amount){ // dest : acc
if (enoughFund(amount)){
withdraw(amount); // <- thread can be preempted in the middle of method execution
acc.deposit(amount);
return true;
}
return false;
}
这些问题被称为竞争条件。一个可能的例子是当两个线程试图从同一账户转移资金时。当一个线程确定有enoughFund
进行信用转账时,该线程可能被抢占,同时其他线程可以开始从该账户转移资金。当第一个线程再次开始处理时,它不会仔细检查是否有enoughFunds
来执行信用转移(他已经检查过它,但他的知识可能已经过时),但它会进入下一行执行。这样您可能无法获得一致的结果。您可以更改所有帐户在开头时的总金额。
Cay Horstmann的核心Java书中对这方面有一个非常好的解释 - 这里chapter about synchronization免费提供。它详细描述了您要问的几乎完全相同的问题。
答案 2 :(得分:2)
所有 atomic 数据类型都承诺,您将提供无锁但线程安全访问其值。因此,使用AtomicInteger
超过synchronized
的正当理由之一是,您需要仅保护更新操作,例如
synchronized (lockObj) {
myInt++; // slower than AtomicInteger
}
在这种情况下,AtomicInteger.incrementAndGet()
会更快。但是,如果您的同步范围大于该范围且增量只是其中的一部分,则建议使用具有非原子整数的synchronized
块(在该块内受保护)。
答案 3 :(得分:2)
是的,你是对的。如果对对象的所有访问都是AtomicInteger
(在任何给定时刻,最多只有一个线程将访问其内容),synchronized
将不会给予任何好处。
正如其他人所指出的,当您需要对该变量进行线程安全访问时,使用AtomicInteger
是最佳选择,并且您可以对其执行简单更新。
在这种情况下,您有两个复合操作,transfer_funds
和withdraw
。前者有三个访问权限,后者有两个访问权限。
您希望这些操作本身是原子,即,它们在其他人看来就好像它们是瞬间发生的,它们不能在较小的操作中分解。为实现这一目标,synchronized
是必要的。
最后,我想留下一个(可能)有用的建议。 您应该为每个帐户分配一个唯一标识符。您可能会问,为什么要防止死锁。
假设我们有两个主题T1
和T2
,以及两个帐户a1
和a2
。
<强> T1 强>:
a1.transfer_funds(a2, 42);
<强> T2 强>:
a2.transfer_funds(a1, 00101010);
您可能会遇到以下交错:
T1 -> a1.enoughFund(42)
T1 -> a1.withdraw(42)
T2 -> a2.enoughFund(00101010)
T2 -> a2.withdraw(00101010)
T1 -> a2.deposit(42) // blocks on a2's monitor, because T2 already has it
T2 -> a1.deposit(00101010) // same as above
两个线程无限期地等待彼此,因为你的所有方法都是synchronized
。
在为每个帐户分配标识符时,解决方案将是:
public class Account {
private int balance;
private final int id;
/* Not synchronized */
public boolean transferFunds(Account acc, int amount) {
if (id < acc.getId()) {
synchronized (this) {
synchronized (acc) {
return transfer(acc, amount);
}
}
}
else if (id > acc.getId()) {
synchronized (acc) {
synchronized (this) {
return transfer(acc, amount);
}
}
}
return true; // same id, transfering to self has no effect.
}
private boolean transfer(Account acc, int amount) {
if (balance >= amount) {
balance -= amount;
// This is not synchronized, you may make it private.
acc.depositUnsynchronized(amount);
return true;
}
return false;
}
}
以上实现了有序锁定获取,因此,无论如何,所有线程都会尝试先获取id最低的帐户。如果该帐户正在进行转帐,则在第一次结束之前不会进行其他转帐。
答案 4 :(得分:2)
如果您非常想使用AtomicInteger
,可以写下:
public class Account {
private final AtomicInteger balance = new AtomicInteger(0);
public void deposit(int amount) {
balance.getAndAdd(amount);
}
public boolean withdraw(int amount) {
for (int i; i < SOME_NUMBER_OF_ATTEMPTS; ++i) {
int currentBalance = balance.get();
if (currentBalance < amount) return false;
boolean updated = balance.compareAndSet(currentBalance, currentBalance - amount);
if (updated) return true;
}
}
public boolean transfer(int amount, Account recipient) {
boolean withdrawn = withdraw(amount);
if (withdrawn) recipient.deposit(amount);
return withdrawn;
}
}
这是安全的,它不使用锁。转移或撤回的线程不能保证完成这样做,但是嘿。
循环比较和设置的技术是标准的。这就是synchronized
使用的锁本身的实现方式。