使用AtomicInteger多线程取款时无法控制原子行为,求代码修正
解决AtomicInteger处理AccountDanger案例时的超额取款问题
嘿,我完全懂你遇到的坑!虽然你用了java.util.concurrent.atomic.AtomicInteger来存储余额,但单个原子操作的组合并不具备原子性,这就是导致超额取款的核心原因。
问题根源分析
你的代码里,run()方法的逻辑是先判断余额是否足够,再休眠,最后执行取款。这两步之间有明显的时间窗口(就是那个Thread.sleep(500)),另一个线程完全可以在这个间隙里修改余额:
- 举个典型场景:当余额只剩10时,线程A判断
10 >=10成立,然后进入休眠;此时线程B也判断10>=10成立,同样休眠。等两个线程醒来后,都会执行取款操作,余额直接变成10-10-10=-10,超额问题就出现了。
方案1:用AtomicInteger的CAS操作实现原子性判断+取款
CAS(Compare-And-Set)是原子类的核心机制,我们可以用它把「检查余额+更新余额」合并成一个原子操作。修改你的Account类,新增一个原子取款方法:
package p1; import java.util.concurrent.atomic.AtomicInteger; public class Account { private AtomicInteger balance = new AtomicInteger(100); public int getBalance() { return balance.get(); } // 原子性取款:返回是否取款成功 public boolean withdraw(int amount) { while (true) { int currentBalance = balance.get(); // 先判断余额是否足够 if (currentBalance < amount) { return false; } // CAS尝试更新:如果当前余额还是currentBalance,就扣减金额 if (balance.compareAndSet(currentBalance, currentBalance - amount)) { System.out.println("~~~~~~ " + balance.get()); return true; } // CAS失败说明其他线程修改了余额,循环重试 } } }
然后修改AccountDanger的run()方法,直接调用原子取款逻辑:
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++) { boolean withdrawSuccess = account.withdraw(amt); if (withdrawSuccess) { System.out.println(Thread.currentThread().getName() + " is going to withdraw.."); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } 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:用synchronized保证操作原子性
如果你觉得CAS的循环逻辑有点绕,也可以用传统的synchronized锁包裹整个业务逻辑,同样能避免竞态条件。比如修改Account的取款方法:
public synchronized void withdraw(int amount) { if (balance.get() >= amount) { balance.addAndGet(-amount); System.out.println("~~~~~~ " + balance); } }
然后在AccountDanger的run()方法里,把判断和取款逻辑放到同步块中:
public void run() { for (int i = 0; i < 10; i++) { synchronized (account) { if (account.getBalance() >= amt) { System.out.println(Thread.currentThread().getName() + " is going to withdraw.."); account.withdraw(amt); } else { System.out.println("not enough balance"); } } try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if (account.getBalance() < 0) { System.out.println("account is over withdrawn!!!"); } } }
核心总结
原子类只能保证单个操作的原子性,但业务逻辑往往是多个操作的组合(比如先判断后修改),这时候必须把整个组合逻辑变成原子操作——不管用CAS还是锁,都能解决这个问题。我当年学并发的时候也踩过同样的坑,以为用了原子类就万事大吉,后来才明白组合操作的原子性才是关键!
内容的提问来源于stack exchange,提问作者JPG
相关产品推荐
相关产品推荐

