10

假设我想实现一个非常简单的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;
    }
}
4

5 回答 5

8

将您的金额声明为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 的 Core Java 书中对这方面有很好的解释——这里是免费提供的关于同步的章节。它详细描述了您所询问的几乎完全相同的问题。

于 2013-06-22T17:16:07.930 回答
7

是的,两者都是,让它同步是一个好主意,并且不需要 Atomic。

如果您仅依赖 Atomic 而不是同步,则可能会遇到以下问题:

    if (enoughFund(amount)){
        withdraw(amount);
        acc.deposit(amount);
        return true;
    }

因为 Atomic 只保证您的整数不会被同时访问,这意味着即使它是由其他线程写入的,enoughFund(amount)也可以保证提供正确的值。amount但是,Atomic 本身并不能保证在这一行得到的值与下一行代码中的值相同,因为另一个线程可以在这两行之间执行另一个 Atomic 操作,从而withdraw(amount);可以在下面设置你的余额零。

于 2013-06-22T17:17:34.253 回答
5

如果你非常想使用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实现的。

于 2013-06-22T19:02:33.070 回答
4

是的,你是对的。AtomicInteger如果对对象的所有访问都是synchronized(在任何给定时刻最多只有一个线程会访问其内容),则不会授予任何好处。

正如其他人指出的那样,AtomicInteger当您需要对该变量进行线程安全访问并且您对其执行简单更新时,最好使用 。
在这种情况下,您有两个复合操作,transfer_funds并且withdraw. 前者有三个访问,后者有两个。

您希望这些操作本身是原子的,,它们在其他人看来好像它们是瞬间发生的,它们不能在较小的操作中分解。要做到这一点,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最小的账户。如果该账户正在进行转账,则在第一次转账结束之前不会发生其他转账。

于 2013-06-22T18:27:48.867 回答
3

原子数据类型向您承诺的只是提供对其的无锁线程安全的访问。因此,您使用AtomicIntegerover的正当理由之一synchronized是您只需要保护更新操作,例如

synchronized (lockObj) {
    myInt++; // slower than AtomicInteger
}

在这种情况下,AtomicInteger.incrementAndGet()会更快。但是,如果您的同步范围大于该范围并且增量只是其中的一部分,则建议使用synchronized具有非原子整数的块(在该块内受保护)。

于 2013-06-22T17:30:06.280 回答