在减去值时无法控制AtomicInteger的原子行为

时间:2018-01-04 10:42:41

标签: java multithreading atomicinteger

这是我第一次在java.util.concurrent.atomic中使用multithreading包进行同步。我试图通过AccountDanger来处理Kathy Sierra为OCP提供的java.util.concurrent.atomic课程的典型例子。但我无法保护balance变量,即AtomicInteger不被撤回。以下是我的代码:

帐户类

package p1;

import java.util.concurrent.atomic.AtomicInteger;

public class Account {
  private AtomicInteger balance = new AtomicInteger(100);

  public int getBalance() {
    return balance.get();
  }

  public void withdraw(int amount) {
    balance.addAndGet(-amount);
    System.out.println("~~~~~~ " + balance);
  }
}

AccountDanger类

package p1;

public class AccountDanger implements Runnable {
  private Account account = new Account();
  private int amt = 10;

  public void run() {

    for (int i = 0; i < 10; i++) {
      if (account.getBalance() >= amt) {
        System.out.println(Thread.currentThread().getName() + " is going to withdraw..");
        try {
          Thread.sleep(500);
        } catch (InterruptedException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
        }
        account.withdraw(amt);
      } else {
        System.out.println("not enough balance");
      }
      if (account.getBalance() < 0) {
        System.out.println("account is over withdrawn!!!");
      }
    }
  }

  public static void main(String[] args) throws InterruptedException {
    AccountDanger ad = new AccountDanger();
    Thread t1 = new Thread(ad, "Mark");
    Thread t2 = new Thread(ad, "Phew");
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println("final balance left is : " + ad.account.getBalance());
  }
}

我知道,我在某处肯定是错的。任何人都可以纠正我的解决方案..

2 个答案:

答案 0 :(得分:3)

您没有原子地使用AtomicInteger(或者更确切地说,Account)。

if(account.getBalance()>=amt){
   // Other stuff
   account.withdraw(amt);

其他线程可以改变“其他东西”内的平衡。

Account中,您需要使用AtomicInteger.compareAndSet

这样的方法
boolean withdrawIfBalanceEquals(int balance, int amt) {
  return this.balance.compareAndSet(balance, balance - amt);
}

AtomicInteger设置为balance-amt,如果其值仍然等于balance,则返回true。如果您在阅读其值后进行了更改,则compareAndSet将返回false

然后,在AccountDanger中,您可以使用它:

int balance = account.getBalance();
if (balance >= amt) {
  // Other stuff.
  account.withdrawIfBalanceEquals(balance, amt);
}

我决定如何最好地处理withdrawIfBalanceEquals返回false的情况;一种方法是:

while (true) {
  int balance = account.getBalance();
  if (balance >= amt) {
    // Other stuff.
    if (account.withdrawIfBalanceEquals(balance, amt)) {
      // If true, we don't have to keep looping: we withdrew the
      // balance.
      break;
    }

    // If false, keep going: you'll read the balance again,
    // check if it's still greater than balance etc.

  } else {
    System.out.println("Not enough balance");
    break;
  }
}

答案 1 :(得分:1)

问题是,在您检查余额(if(account.getBalance()>=amt) {)的时间点与实际提取的时间点(account.withdraw(amt);)之间,余额可能已发生变化,例如降低到<= amt

让我们看一个明确的具体方案。如果您有一个余额为500的帐户,并且您开始两个线程分别撤销300,那么每个交易本身都是合法的,因为它不会透支,因此对{{1}进行两次检查} -statement将产生if。但交易并不相互了解。第一笔交易将余额true降低为300。然后,第二笔交易将余额降低到200

要解决此问题,您需要引入一些同步。一种可能性是在退出前检查余额:

-100

正如您所看到的,我介绍了一个public void withdraw (int amount) { if (balance.get() > amount) { balance.addAndGet(-amount); System.out.println("~~~~~~ "+balance); } else { throw new InsufficientBalanceException(); } } 来表明交易无法执行。您可以自由地为此案例介绍您自己的代码。这缩短了检查和实际更改之间的时间,但并未完全解决问题。为此,您必须确保在一个线程检查并可能修改余额时,没有其他线程可以。强制执行此操作的一种方法是synchronized keyword

Exception

现在,一次只能有一个线程在一个实例上调用public synchronized void withdraw(int amount) { if (balance.get() > amount) { balance.addAndGet(-amount); System.out.println("~~~~~~ "+balance); } else { throw new InsufficientBalanceException(); } }

正如@AndyTurner指出的那样,如果你已经拥有一个线程安全的数据结构,比如withdraw(...)并且必须围绕它AtomicInteger,那么可能会有一些可疑的东西。 synchronize上的每个单独操作都是原子操作,但不是顺序的多个操作,这就是我们必须引入一些同步的原因。在这种特殊情况下,您可以删除AtomicInteger并将其替换为AtomicInteger