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就够了,但其实问题出在:
retraitArgent方法本身确实是线程安全的——同一时间只有一个线程能执行它的扣减逻辑;- 但**
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
相关产品推荐
相关产品推荐

