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

Java同步方法未按预期工作:账户余额出现负值问题

问题分析:为什么同步方法仍导致账户余额为负?

我来帮你理清问题所在——你遇到的情况其实是典型的竞态条件,虽然synchronized修饰的方法本身是线程安全的,但你的业务逻辑里有个关键漏洞:检查余额和执行取款这两步操作没有被原子化。

先回顾下你的代码和问题现象:

你的代码

CompteEnBanque 类

package runnableThread;

public class CompteEnBanque {
    private int solde = 100;

    public int getSolde(){
        if(this.solde < 0)
            System.out.println(solde);
        return this.solde;
    }

    public synchronized void retraitArgent(int retrait){
        solde = solde - retrait;
        System.out.println("Solde = " + solde);
    }
}

RunImpl 线程类

package runnableThread;

public class RunImpl implements Runnable {
    private CompteEnBanque cb;
    private String name;

    public RunImpl(CompteEnBanque cb, String name){
        this.cb = cb;
        this.name = name;
    }

    public void run() {
        for(int i = 0; i < 50; i++){
            if(cb.getSolde() >= 2){
                cb.retraitArgent(2);
                System.out.println("Retrait effectué par " + this.name);
            }
        }
    }
}

Test 主类

package runnableThread;

public class Test {
    public static void main(String[] args) {
        CompteEnBanque cb = new CompteEnBanque();
        Thread t = new Thread(new RunImpl(cb, "marie"));
        Thread t2 = new Thread(new RunImpl(cb, "philippe"));
        t.start();
        t2.start();
    }
}

问题现象(你提供的部分输出)

Retrait effectué par marie
Solde = 0
Retrait effectué par marie
Solde = -2
Retrait effectué par philippe
-2

为什么会出现负数?

你可能以为synchronized修饰retraitArgent就够了,但其实问题出在:

  1. retraitArgent方法本身确实是线程安全的——同一时间只有一个线程能执行它的扣减逻辑;
  2. 但**cb.getSolde() >=2这个检查操作,和后续的retraitArgent(2)执行,这两步之间没有任何锁保护**!

举个具体的场景:

  • 当账户余额还剩2的时候,线程A和线程B同时执行了cb.getSolde(),都得到结果2,都通过了>=2的判断;
  • 线程A先抢到锁,执行retraitArgent(2),余额变成0;
  • 线程A释放锁后,线程B抢到锁,执行retraitArgent(2),余额直接变成-2。

说白了,两个线程都"认为"余额足够取款,然后依次执行扣减,就导致了超额扣除。


解决方案:把检查+取款变成原子操作

要解决这个问题,必须把检查余额和执行取款这整个逻辑放在同一个锁的保护下,确保同一时间只有一个线程能完成"检查-扣减"的完整流程。这里有两种常用方式:

方式一:把检查逻辑移到同步方法内部

修改CompteEnBanque类,新增一个包含完整逻辑的同步方法,让整个操作在锁内完成:

package runnableThread;

public class CompteEnBanque {
    private int solde = 100;

    public int getSolde() {
        if (this.solde < 0)
            System.out.println(solde);
        return this.solde;
    }

    // 新增:同步方法,原子完成"检查余额+扣减"
    public synchronized boolean retraitArgentSiPossible(int retrait) {
        if (solde >= retrait) {
            solde -= retrait;
            System.out.println("Solde = " + solde);
            return true;
        }
        return false;
    }

    // 原方法可以保留,也可以根据需求调整
    public synchronized void retraitArgent(int retrait){
        solde = solde - retrait;
        System.out.println("Solde = " + solde);
    }
}

然后修改RunImpl的run方法,调用新的同步方法:

public void run() {
    for (int i = 0; i < 50; i++) {
        if (cb.retraitArgentSiPossible(2)) {
            System.out.println("Retrait effectué par " + this.name);
        }
    }
}

方式二:在线程逻辑中对共享账户加锁

如果不想修改CompteEnBanque类,可以在RunImpl的run方法里,对共享的cb对象加锁,确保检查和取款是原子操作:

public void run() {
    for (int i = 0; i < 50; i++) {
        synchronized (cb) { // 对共享账户对象加锁,包裹检查+取款逻辑
            if (cb.getSolde() >= 2) {
                cb.retraitArgent(2);
                System.out.println("Retrait effectué par " + this.name);
            }
        }
    }
}

总结

核心问题就是跨方法的"检查-执行"操作没有被原子化,单个方法的同步不足以覆盖整个业务逻辑的线程安全需求。只要把需要原子执行的逻辑块放在同一个锁的保护下,就能避免这种竞态条件,保证余额不会变成负数。

内容的提问来源于stack exchange,提问作者lemahdois

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 09:49:05