使用AtomicInteger时同步

Ari*_*deh 6 java concurrency multithreading

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

它是一个好主意,使下面的方法synchronized,即使balanceAtomicInteger

另一方面,如果我们将所有方法都同步化,就不再使用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;
    }
}
Run Code Online (Sandbox Code Playgroud)

i C*_*ood 7

对两者都是肯定的,将它同步化是一个好主意,并且不需要Atomic.

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

    if (enoughFund(amount)){
        withdraw(amount);
        acc.deposit(amount);
        return true;
    }
Run Code Online (Sandbox Code Playgroud)

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


omn*_*nom 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;
    }
Run Code Online (Sandbox Code Playgroud)

这些问题被称为竞争条件.一个可能的例子是当两个线程试图从同一账户转移资金时.当一个线程确定enoughFund要执行信用转账时,该线程可能被抢占,同时其他线程可以开始从该账户转移资金.当第一个线程再次开始处理时,它不会仔细检查是否enoughFunds要执行信用转移(他已经检查过,但他的知识可能已经过时),但它会进入下一行执行.这样您可能无法获得一致的结果.您可以更改所有帐户在开头时的总金额.

Cay Horstmann的核心Java书中对这方面有一个非常好的解释 - 这里有关于免费同步的章节.它详细描述了您要问的几乎完全相同的问题.


Rav*_*yal 5

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

synchronized (lockObj) {
    myInt++; // slower than AtomicInteger
}
Run Code Online (Sandbox Code Playgroud)

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


afs*_*tos 5

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

AtomicInteger正如其他人指出的那样,当您需要对该变量进行线程安全访问并且对其执行简单更新时,最好使用。
在本例中,您有两个复合运算transfer_fundswithdraw。前者有三个访问权限,后者有两个访问权限。

您希望这些操作本身是原子的,它们对其他人来说似乎是瞬时发生的,它们不能分解为更小的操作。要实现这一点,synchronized是必要的。


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

假设我们有两个线程T1T2,以及两个帐户a1a2

T1

a1.transfer_funds(a2, 42);
Run Code Online (Sandbox Code Playgroud)

T2

a2.transfer_funds(a1, 00101010);
Run Code Online (Sandbox Code Playgroud)

您可能会遇到以下交错:

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
Run Code Online (Sandbox Code Playgroud)

两个线程都会无限期地等待对方,因为您的所有方法都是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;
    }
}
Run Code Online (Sandbox Code Playgroud)

上面实现了有序的锁获取,因此,无论哪种情况,所有线程都会先尝试获取id最低的账户。如果该帐户正在进行转账,则在第一个转账结束之前不会发生其他转账。


Tom*_*son 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

这是安全的,它不使用锁。进行转账或取款的线程并不能保证一定会完成,但是嘿。

循环比较并设置的技术是一种标准技术。这就是所使用的锁synchronized本身是如何实现的。