使用AtomicInteger时同步

时间:2013-06-22 17:10:49

标签: java multithreading concurrency

让我们假设我想实现一个非常简单的Bank Account类,我们想要关注并发和多线程问题,

即使synchronizedbalance,制作以下方法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;
    }
}

5 个答案:

答案 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_fundswithdraw。前者有三个访问权限,后者有两个访问权限。

您希望这些操作本身是原子,它们在其他人看来就好像它们是瞬间发生的,它们不能在较小的操作中分解。为实现这一目标,synchronized是必要的。


最后,我想留下一个(可能)有用的建议。 您应该为每个帐户分配一个唯一标识符。您可能会问,为什么要防止死锁。

假设我们有两个主题T1T2,以及两个帐户a1a2

<强> 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使用的锁本身的实现方式。

相关问题