【问题标题】:Synchronization while using AtomicInteger使用 AtomicInteger 时的同步
【发布时间】:2013-06-22 17:10:48
【问题描述】:

假设我要实现一个非常简单的Bank Account 类,并且我们要处理并发和多线程问题,

即使 balanceAtomicInteger ,创建以下方法 synchronized 是否是个好主意?

另一方面,如果我们将所有方法都设为同步,那么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;
    }
}

【问题讨论】:

    标签: java multithreading concurrency


    【解决方案1】:

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

    【讨论】:

    • 所以没有必要使用AtomicInteger
    • @mr5,实际上是的——在这种情况下,使用原子原始包装器(AtomcIntegerAtomicBoolean 等)是过度的,因为它们没有提供任何真正的保护来防止所描述的赛车条件.
    【解决方案2】:

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

    如果你只依赖 Atomic 而不是同步,你可能会遇到这样的问题:

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

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

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

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

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

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

        【讨论】:

        • 所以在这里,我们只需要将withdraw()checkBalance()同步,对吧?
        • 如果你保留AtomicInteger,是的。如果您计算您的方法对balance 的引用,您会看到所有方法,除了withdraw 和transfer,只引用balance 一次,然后使用该访问的结果。例如,Withdraw 有两个引用(检查和减法),因此没有同步是不安全的(除非您实现 Tom Anderson 在他的回答中描述的内容)。
        【解决方案5】:

        atomic 数据类型向您承诺的唯一方法就是让 lock-freethread-safe 访问它的值。因此,您使用AtomicInteger 而不是synchronized 的正当理由之一是您只需要保护您的更新操作,例如

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

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

        【讨论】:

          猜你喜欢
          • 2021-03-18
          • 2015-12-02
          • 1970-01-01
          • 2020-12-06
          • 1970-01-01
          • 2012-07-25
          • 1970-01-01
          • 2013-04-03
          • 1970-01-01
          相关资源
          最近更新 更多