Java多线程环境下账户转账实现正确性排查及优化问询
Hey there, let's break down what's going wrong with your multi-threaded transfer setup and how to fix it to keep that total account balance rock-solid consistent!
Core Issues in Your Current Implementation
Let's start with the biggest red flags that are causing your balance drift:
Java Object Locks Are Useless for Database Sync
You're trying to synchronize onAccountobjects loaded from Hibernate, but every call toopenSession().get(...)creates a new Session, which loads a fresh, independent instance of the Account. That means Thread A's lock on "Account 1" doesn't affect Thread B's lock on the same database account (they're different Java objects!). Your initial "success" with small thread counts/slow execution was just luck—threads weren't overlapping, not because the locks worked.Misaligned Transactions & Sessions
You load accounts in separate Sessions, modify them in a sync block, then open another Session to commit changes. This breaks ACID guarantees: there's no link between the modified objects and the final update Session, so Hibernate can't track changes properly, leading to dirty writes.Account Method Logic Is Reversed
This is a critical bug that's hiding in plain sight:widrawal(int sum)adds money to the account (should subtract for a withdrawal)send(int sum)subtracts money from the account (should add for a deposit)
While this might keep total balance the same on paper, it completely reverses your transfer direction and makes your balance check logic misleading.
Fixed Implementation
Let's rewrite the key parts to fix these issues, using database-level locking and proper Hibernate transaction management.
1. Corrected Account Entity
First, fix the method logic and naming:
@Entity public class Account { @Id private int id; private int money; public Account() { } // Withdraw (remove funds from this account) public void withdrawal(int sum) { money -= sum; } // Deposit (add funds to this account) public void deposit(int sum) { money += sum; } // Getters & Setters public int getId() { return id; } public void setId(int id) { this.id = id; } public int getMoney() { return money; } public void setMoney(int money) { this.money = money; } }
2. Transfer Logic with Pessimistic Locking
We'll use database-level pessimistic write locks to ensure only one thread can modify an account at a time, and wrap the entire operation in a single transaction:
public class Transfer { public void transaction() { int transferAmount = (int) (Math.random() * 100) + 10; int fromId = (int) (Math.random() * 50) + 1; int toId = (int) (Math.random() * 50) + 1; if (fromId == toId) { System.out.println("Skipping transfer: same account ID"); return; } // Enforce lock order (smaller ID first) to avoid deadlocks int firstId = Math.min(fromId, toId); int secondId = Math.max(fromId, toId); boolean isFromFirst = fromId == firstId; Session session = null; Transaction tx = null; try { session = HibernateSessionFactoryUtil.getSessionFactory().openSession(); tx = session.beginTransaction(); // Load accounts with pessimistic write locks (locks the database row) Account fromAccount = session.get(Account.class, isFromFirst ? firstId : secondId, LockMode.PESSIMISTIC_WRITE); Account toAccount = session.get(Account.class, isFromFirst ? secondId : firstId, LockMode.PESSIMISTIC_WRITE); // Validate sufficient funds if (fromAccount.getMoney() < transferAmount) { System.out.println("Insufficient funds: Account " + fromAccount.getId() + " has " + fromAccount.getMoney() + ", needs " + transferAmount); tx.rollback(); return; } // Execute transfer fromAccount.withdrawal(transferAmount); toAccount.deposit(transferAmount); // Hibernate auto-tracks changes from the same session—no need for manual update tx.commit(); System.out.println("Successfully transferred " + transferAmount + " from Account " + fromAccount.getId() + " to Account " + toAccount.getId()); } catch (Exception e) { if (tx != null) tx.rollback(); System.err.println("Transfer failed: " + e.getMessage()); e.printStackTrace(); } finally { if (session != null) session.close(); } } }
3. Simplified Transfer Thread
No need for AtomicInteger—just a straightforward loop, and handle interrupts properly:
public class TransferThread extends Thread { private final Transfer transfer = new Transfer(); @Override public void run() { for (int i = 0; i < 10; i++) { try { transfer.transaction(); // Random sleep between 100-500ms int sleepTime = (int) (Math.random() * 400) + 100; Thread.sleep(sleepTime); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.out.println("Thread interrupted, stopping transfers"); break; } } } }
Key Takeaways to Keep Balance Consistent
- Use Database-Level Locking: Never rely on Java object locks for database sync—Hibernate creates object copies per Session, so those locks don't cross thread boundaries. Pessimistic locks (
LockMode.PESSIMISTIC_WRITE) ensure the database itself enforces mutual exclusion. - Wrap Everything in One Transaction: Load, modify, and commit must happen in the same Session/transaction to guarantee ACID compliance.
- Enforce Lock Order: Sort account IDs before locking to avoid deadlocks (two threads waiting for each other's locks).
- Handle Exceptions & Rollbacks: Always roll back transactions on failure to prevent partial updates from corrupting your data.
内容的提问来源于stack exchange,提问作者Yury

