You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 07:13:47