模拟夫妻同时取款的线程安全问题
一、问题分析
- 账户余额:10万元
- 小周:取10万元
- 小红:取10万元
- 两人同时取:可能都取成功(线程安全问题)
二、错误示例(线程不安全)
public class UnsafeBankAccount {
private double balance = 100000; // 初始余额10万
// 取款方法(线程不安全)
public void withdraw(double amount) {
if (balance >= amount) {
System.out.println(Thread.currentThread().getName() + " 开始取款");
// 模拟网络延迟
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= amount;
System.out.println(Thread.currentThread().getName() +
" 取款成功,取款金额: " + amount + ",余额: " + balance);
} else {
System.out.println(Thread.currentThread().getName() +
" 取款失败,余额不足");
}
}
public static void main(String[] args) {
UnsafeBankAccount account = new UnsafeBankAccount();
// 小明线程
Thread xiaoming = new Thread(() -> {
account.withdraw(100000);
}, "小明");
// 小红线程
Thread xiaohong = new Thread(() -> {
account.withdraw(100000);
}, "小红");
// 同时启动(模拟同时取款)
xiaoming.start();
xiaohong.start();
try {
xiaoming.join();
xiaohong.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("\n最终余额: " + account.balance);
}
}
运行结果(可能):
小明 开始取款
小红 开始取款
小明 取款成功,取款金额: 100000.0,余额: 0.0
小红 取款成功,取款金额: 100000.0,余额: -100000.0
最终余额: -100000.0
问题:两人都取款成功,余额变负数!
三、解决方案
方案1:synchronized同步方法
public class SafeBankAccount1 {
private double balance = 100000;
// 同步取款方法
public synchronized void withdraw(double amount) {
if (balance >= amount) {
System.out.println(Thread.currentThread().getName() + " 开始取款");
try {
Thread.sleep(100); // 模拟处理时间
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= amount;
System.out.println(Thread.currentThread().getName() +
" 取款成功,取款金额: " + amount + ",余额: " + balance);
} else {
System.out.println(Thread.currentThread().getName() +
" 取款失败,余额不足");
}
}
}
方案2:synchronized同步代码块
public class SafeBankAccount2 {
private double balance = 100000;
private final Object lock = new Object(); // 专用锁对象
public void withdraw(double amount) {
synchronized(lock) { // 同步代码块
if (balance >= amount) {
System.out.println(Thread.currentThread().getName() + " 开始取款");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= amount;
System.out.println(Thread.currentThread().getName() +
" 取款成功,取款金额: " + amount + ",余额: " + balance);
} else {
System.out.println(Thread.currentThread().getName() +
" 取款失败,余额不足");
}
}
}
}
方案3:使用ReentrantLock
import java.util.concurrent.locks.ReentrantLock;
public class SafeBankAccount3 {
private double balance = 100000;
private final ReentrantLock lock = new ReentrantLock();
public void withdraw(double amount) {
lock.lock(); // 获取锁
try {
if (balance >= amount) {
System.out.println(Thread.currentThread().getName() + " 开始取款");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= amount;
System.out.println(Thread.currentThread().getName() +
" 取款成功,取款金额: " + amount + ",余额: " + balance);
} else {
System.out.println(Thread.currentThread().getName() +
" 取款失败,余额不足");
}
} finally {
lock.unlock(); // 必须释放锁
}
}
}
四、完整测试程序
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class CoupleWithdrawDemo {
public static void main(String[] args) {
System.out.println("=== 夫妻同时取款模拟 ===");
System.out.println("初始余额: 100000元");
System.out.println("小明取款: 100000元");
System.out.println("小红取款: 100000元\n");
// 测试不同方案
testUnsafeAccount(); // 线程不安全
testSafeAccountSync(); // synchronized方案
testSafeAccountLock(); // ReentrantLock方案
}
/**
* 测试线程不安全账户
*/
private static void testUnsafeAccount() {
System.out.println("\n--- 测试1: 线程不安全账户 ---");
UnsafeBankAccount account = new UnsafeBankAccount();
testWithTwoPeople(account);
}
/**
* 测试synchronized安全账户
*/
private static void testSafeAccountSync() {
System.out.println("\n--- 测试2: synchronized安全账户 ---");
SafeBankAccount1 account = new SafeBankAccount1();
testWithTwoPeople(account);
}
/**
* 测试ReentrantLock安全账户
*/
private static void testSafeAccountLock() {
System.out.println("\n--- 测试3: ReentrantLock安全账户 ---");
SafeBankAccount3 account = new SafeBankAccount3();
testWithTwoPeople(account);
}
/**
* 通用测试方法:模拟两人同时取款
*/
private static void testWithTwoPeople(BankAccount account) {
ExecutorService executor = Executors.newFixedThreadPool(2);
// 小明取款
executor.execute(() -> {
account.withdraw(100000);
});
// 小红取款
executor.execute(() -> {
account.withdraw(100000);
});
executor.shutdown();
try {
executor.awaitTermination(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 银行账户接口
*/
interface BankAccount {
void withdraw(double amount);
}
/**
* 线程不安全账户实现
*/
class UnsafeBankAccount implements BankAccount {
private double balance = 100000;
@Override
public void withdraw(double amount) {
if (balance >= amount) {
System.out.println(Thread.currentThread().getName() + " 检查余额: 足够");
try {
// 模拟网络延迟、处理时间等
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= amount;
System.out.println(Thread.currentThread().getName() +
" ✓ 取款成功,取款金额: " + amount + ",余额: " + balance);
} else {
System.out.println(Thread.currentThread().getName() +
" ✗ 取款失败,余额不足,当前余额: " + balance);
}
}
}
/**
* synchronized安全账户实现
*/
class SafeBankAccount1 implements BankAccount {
private double balance = 100000;
@Override
public synchronized void withdraw(double amount) {
if (balance >= amount) {
System.out.println(Thread.currentThread().getName() + " 检查余额: 足够");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= amount;
System.out.println(Thread.currentThread().getName() +
" ✓ 取款成功,取款金额: " + amount + ",余额: " + balance);
} else {
System.out.println(Thread.currentThread().getName() +
" ✗ 取款失败,余额不足,当前余额: " + balance);
}
}
}
/**
* ReentrantLock安全账户实现
*/
class SafeBankAccount3 implements BankAccount {
private double balance = 100000;
private final java.util.concurrent.locks.ReentrantLock lock =
new java.util.concurrent.locks.ReentrantLock();
@Override
public void withdraw(double amount) {
lock.lock();
try {
if (balance >= amount) {
System.out.println(Thread.currentThread().getName() + " 检查余额: 足够");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= amount;
System.out.println(Thread.currentThread().getName() +
" ✓ 取款成功,取款金额: " + amount + ",余额: " + balance);
} else {
System.out.println(Thread.currentThread().getName() +
" ✗ 取款失败,余额不足,当前余额: " + balance);
}
} finally {
lock.unlock();
}
}
}
五、运行结果分析
=== 夫妻同时取款模拟 ===
初始余额: 100000元
小明取款: 100000元
小红取款: 100000元
--- 测试1: 线程不安全账户 ---
pool-1-thread-1 检查余额: 足够
pool-1-thread-2 检查余额: 足够
pool-1-thread-1 ✓ 取款成功,取款金额: 100000.0,余额: 0.0
pool-1-thread-2 ✓ 取款成功,取款金额: 100000.0,余额: -100000.0
--- 测试2: synchronized安全账户 ---
pool-2-thread-1 检查余额: 足够
pool-2-thread-1 ✓ 取款成功,取款金额: 100000.0,余额: 0.0
pool-2-thread-2 ✗ 取款失败,余额不足,当前余额:
三种方案的优缺点和最佳方案分析
一、方案对比详细分析
方案1:synchronized同步方法
优点:
1. **实现简单** - 只需加一个关键字
public synchronized void withdraw(double amount) {
// 方法体自动同步
}
2. **自动管理锁** - JVM自动加锁/释放锁
// 进入方法自动加锁,退出方法自动释放
// 包括异常退出也会自动释放锁
3. **可重入** - 同一线程可重复获取锁
public synchronized void methodA() {
methodB(); // 可以调用另一个同步方法
}
public synchronized void methodB() {
// 同一线程不会死锁
}
4. **保证可见性** - 自动处理内存屏障
private double balance; // 对synchronized方法可见
缺点:
1. **性能较差** - 锁粒度较粗
public synchronized void withdraw(double amount) {
// 整个方法被锁定,即使只有部分代码需要同步
checkValid(); // 不需要同步
validateAmount(); // 不需要同步
updateBalance(); // 需要同步 ← 应该只锁这里
sendNotification(); // 不需要同步
}
2. **无法中断** - 线程会一直阻塞等待
// 线程A获取锁
// 线程B等待锁 ← 无法中断,只能一直等
3. **无法设置超时** - 可能造成死锁
// 如果持有锁的线程卡住,其他线程永远等不到
4. **不够灵活** - 只能有一个条件等待队列
// 所有等待线程都在同一个队列
方案2:synchronized同步代码块
优点:
1. **锁粒度细** - 只同步必要代码
public void withdraw(double amount) {
// 非同步代码
checkValid();
validateAmount();
synchronized(this) { // 只锁核心部分
updateBalance();
}
// 非同步代码
sendNotification();
}
2. **可以使用不同锁对象** - 提高并发度
private final Object balanceLock = new Object();
private final Object logLock = new Object();
public void transfer() {
synchronized(balanceLock) { // 只锁余额
updateBalance();
}
synchronized(logLock) { // 只锁日志
writeLog();
}
}
3. **减少锁竞争** - 不同资源用不同锁
缺点:
1. **需要手动选择锁对象** - 容易出错
// ❌ 错误:锁错了对象
synchronized(new Object()) { // 每次锁不同对象
// 实际上没有同步效果
}
2. **代码复杂度增加** - 需要显式管理锁范围
synchronized(lock) {
if (condition) {
return; // 可能忘记某些清理
}
// 业务逻辑
}
3. **和synchronized方法有同样的其他限制**
方案3:ReentrantLock
优点:
1. **功能强大** - 支持多种特性
ReentrantLock lock = new ReentrantLock();
lock.lockInterruptibly(); // 可中断获取锁
lock.tryLock(1, TimeUnit.SECONDS); // 尝试获取,可超时
2. **公平锁支持** - 避免线程饥饿
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
// 按请求顺序获取锁
3. **多个条件变量** - 更精细的线程通信
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
// 不同条件可以分开等待/唤醒
4. **性能更好** - 在竞争激烈时表现更好
// 使用CAS和队列优化
5. **锁信息可查询** - 便于调试
lock.isLocked(); // 锁是否被持有
lock.isHeldByCurrentThread(); // 当前线程是否持有
lock.getQueueLength(); // 等待队列长度
缺点:
1. **必须手动释放锁** - 容易忘记
lock.lock();
try {
// 业务逻辑
if (error) {
return; // ❌ 可能忘记释放锁
}
} finally {
lock.unlock(); // ✅ 必须放在finally中
}
2. **代码更复杂** - 需要try-finally块
// synchronized只需要一个关键字
// ReentrantLock需要4行模板代码
3. **容易死锁** - 如果解锁顺序不对
lock1.lock();
lock2.lock(); // 可能死锁
// 应该总是按固定顺序获取锁
4. **学习成本高** - 需要理解更多概念
二、银行取款场景最佳方案分析
场景需求分析:
// 银行账户取款要求:
1. **线程安全** - 绝对保证数据一致性
2. **性能中等** - 并发不会特别高
3. **简单可靠** - 金融系统稳定第一
4. **便于维护** - 代码要清晰易懂
方案评分(1-5分):
| 评估维度 | synchronized方法 | synchronized块 | ReentrantLock |
|---|---|---|---|
| 安全性 | 5分(自动管理) | 5分 | 4分(需手动释放) |
| 性能 | 3分(锁粒度粗) | 4分 | 5分 |
| 代码简洁 | 5分(最简单) | 4分 | 3分 |
| 灵活性 | 2分(功能有限) | 3分 | 5分 |
| 可维护性 | 5分(最易懂) | 4分 | 3分 |
| 总分 | 20分 | 20分 | 20分 |
三、最佳方案推荐:synchronized代码块
为什么选择synchronized代码块?
public class BankAccount {
private double balance = 100000;
private final Object lock = new Object(); // 专用锁对象
/**
* 最佳实践:synchronized代码块
* 原因1:锁粒度合适,只保护核心逻辑
* 原因2:代码清晰,容易理解
* 原因3:JVM自动优化,性能足够
* 原因4:避免忘记释放锁的风险
*/
public boolean withdraw(double amount) {
// 参数校验(不需要同步)
if (amount <= 0) {
throw new IllegalArgumentException("金额必须大于0");
}
if (amount > 100000) { // 单笔限额
throw new IllegalArgumentException("超过单笔限额");
}
// 核心业务逻辑需要同步
synchronized(lock) {
// 双重检查(防止条件变化)
if (balance < amount) {
return false;
}
// 模拟业务处理时间
try {
Thread.sleep(10); // 模拟数据库操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
// 更新余额
balance -= amount;
System.out.println(Thread.currentThread().getName() +
" 取款成功,余额: " + balance);
return true;
}
// 后续处理(不需要同步)
// sendSMS(); // 发送短信通知
// writeLog(); // 写入日志
}
}
最佳方案的优点:
-
性能与安全的平衡
// 锁范围最小化 synchronized(lock) { // 只锁必要的2-3行代码 if (balance >= amount) { balance -= amount; } } // 非同步部分可以并发执行 -
避免常见错误
// ✅ 正确:使用专用锁对象 private final Object lock = new Object(); // ❌ 错误:使用字符串常量(可能与其他类冲突) private final String LOCK = "LOCK"; // ❌ 错误:锁this(可能被外部同步干扰) synchronized(this) { } -
便于维护和调试
// 锁对象明确 private final Object balanceLock = new Object(); private final Object auditLock = new Object(); // 不同操作用不同锁,减少竞争 public void complexOperation() { synchronized(balanceLock) { updateBalance(); } synchronized(auditLock) { writeAuditLog(); } }
四、生产级完整实现
版本1:基础版(推荐)
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.atomic.AtomicLong;
/**
* 生产环境银行账户(最佳实践)
*/
public class ProductionBankAccount {
// 余额(使用volatile保证可见性)
private volatile double balance;
// 专用锁对象(final防止被修改)
private final Object balanceLock = new Object();
// 交易流水号生成器
private final AtomicLong transactionIdGenerator = new AtomicLong(1);
// 日期格式化器(线程安全)
private static final DateTimeFormatter DATE_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
public ProductionBankAccount(double initialBalance) {
if (initialBalance < 0) {
throw new IllegalArgumentException("初始余额不能为负数");
}
this.balance = initialBalance;
}
/**
* 安全取款方法(生产级实现)
* @param amount 取款金额(必须大于0)
* @param userId 用户ID(用于日志)
* @return 交易结果对象
*/
public WithdrawalResult safeWithdraw(double amount, String userId) {
// 参数校验(不需要同步)
validateWithdrawal(amount, userId);
long startTime = System.nanoTime();
long transactionId = transactionIdGenerator.getAndIncrement();
try {
// 核心同步代码块(最小范围)
synchronized(balanceLock) {
// 双重检查(防止条件变化)
if (balance < amount) {
return WithdrawalResult.failed(
transactionId, userId, amount, balance,
"余额不足", getCurrentTime()
);
}
// 模拟业务处理(实际是数据库操作)
simulateBusinessProcess();
// 更新余额
double oldBalance = balance;
balance -= amount;
double newBalance = balance;
long endTime = System.nanoTime();
long duration = (endTime - startTime) / 1_000_000; // 毫秒
// 记录成功结果
return WithdrawalResult.success(
transactionId, userId, amount,
oldBalance, newBalance,
getCurrentTime(), duration
);
}
} catch (Exception e) {
// 异常处理
return WithdrawalResult.failed(
transactionId, userId, amount, balance,
"系统错误: " + e.getMessage(), getCurrentTime()
);
}
}
/**
* 参数校验
*/
private void validateWithdrawal(double amount, String userId) {
if (amount <= 0) {
throw new IllegalArgumentException("取款金额必须大于0");
}
if (userId == null || userId.trim().isEmpty()) {
throw new IllegalArgumentException("用户ID不能为空");
}
if (amount > 50000) { // 单笔限额5万
throw new IllegalArgumentException("超过单笔取款限额");
}
}
/**
* 模拟业务处理(实际是数据库操作、风险检查等)
*/
private void simulateBusinessProcess() {
try {
// 模拟各种检查和处理时间
Thread.sleep(5); // 风险检查
Thread.sleep(3); // 反洗钱检查
Thread.sleep(2); // 数据库操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("处理被中断", e);
}
}
/**
* 获取当前时间戳
*/
private String getCurrentTime() {
return LocalDateTime.now().format(DATE_FORMATTER);
}
/**
* 查询余额(不需要同步,因为balance是volatile)
*/
public double getBalance() {
return balance;
}
/**
* 交易结果类(不可变)
*/
public static class WithdrawalResult {
private final long transactionId;
private final String userId;
private final double amount;
private final boolean success;
private final String message;
private final String timestamp;
private final double oldBalance;
private final double newBalance;
private final long processingTimeMs;
// 构造器、工厂方法、getter等...
public static WithdrawalResult success(long transactionId, String userId,
double amount, double oldBalance, double newBalance,
String timestamp, long processingTimeMs) {
return new WithdrawalResult(transactionId, userId, amount,
true, "取款成功", timestamp,
oldBalance, newBalance, processingTimeMs);
}
public static WithdrawalResult failed(long transactionId, String userId,
double amount, double balance,
String reason, String timestamp) {
return new WithdrawalResult(transactionId, userId, amount,
false, reason, timestamp,
balance, balance, 0);
}
}
}
版本2:高级版(带监控)
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* 高级银行账户(带性能监控和超时控制)
* 适用于高并发场景
*/
public class AdvancedBankAccount {
private double balance;
// 使用ReentrantLock(因为需要tryLock和监控)
private final ReentrantLock lock = new ReentrantLock(true); // 公平锁
// 监控指标
private long totalWithdrawals = 0;
private long failedWithdrawals = 0;
private long totalLockWaitTime = 0; // 纳秒
/**
* 高级取款方法(带超时和监控)
*/
public WithdrawalResult advancedWithdraw(double amount, String userId,
long timeout, TimeUnit unit) {
long startTime = System.nanoTime();
boolean locked = false;
try {
// 尝试获取锁(带超时)
locked = lock.tryLock(timeout, unit);
if (!locked) {
// 获取锁超时
failedWithdrawals++;
return WithdrawalResult.failed("获取锁超时");
}
// 记录等待时间
long waitTime = System.nanoTime() - startTime;
totalLockWaitTime += waitTime;
// 业务逻辑
if (balance >= amount) {
simulateProcess();
balance -= amount;
totalWithdrawals++;
return WithdrawalResult.success(amount, balance);
} else {
failedWithdrawals++;
return WithdrawalResult.failed("余额不足");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
failedWithdrawals++;
return WithdrawalResult.failed("操作被中断");
} finally {
if (locked) {
lock.unlock();
}
}
}
/**
* 获取监控指标
*/
public MonitoringStats getMonitoringStats() {
return new MonitoringStats(
totalWithdrawals,
failedWithdrawals,
totalLockWaitTime,
lock.getQueueLength(),
lock.isLocked()
);
}
// 监控统计类
public static class MonitoringStats {
private final long totalWithdrawals;
private final long failedWithdrawals;
private final long totalLockWaitTime;
private final int queueLength;
private final boolean isLocked;
// 构造器、getter等...
}
}
五、选择决策树
需要实现多线程同步吗?
├── 不需要 → 不用任何同步
│
├── 需要 → 继续判断
│ ├── 是简单计数器/标志吗?
│ │ ├── 是 → 使用Atomic类
│ │ └── 否 → 继续判断
│ │
│ ├── 需要高级功能吗?(超时、中断、公平锁)
│ │ ├── 是 → 使用ReentrantLock
│ │ └── 否 → 继续判断
│ │
│ └── 是典型业务逻辑同步吗?
│ ├── 是 → 使用synchronized代码块 ← ★ 推荐
│ └── 否 → 根据具体情况选择
六、最终建议
对小明小红取款问题的最佳方案:
// 方案选择:synchronized代码块
// 原因:
// 1. 银行账户并发不会特别高(不是秒杀场景)
// 2. 代码清晰易懂,维护成本低
// 3. 性能完全足够
// 4. 避免手动管理锁的复杂性
public class BestBankAccount {
private double balance = 100000;
private final Object lock = new Object();
public String withdraw(String person, double amount) {
// 快速失败检查(不需要锁)
if (amount <= 0) {
return person + ": 金额无效";
}
synchronized(lock) {
if (balance >= amount) {
// 模拟处理时间
try { Thread.sleep(50); } catch (InterruptedException e) {}
balance -= amount;
return person + ": 取款成功,余额 " + balance;
} else {
return person + ": 余额不足";
}
}
}
}
// 测试
public static void main(String[] args) throws InterruptedException {
BestBankAccount account = new BestBankAccount();
// 小明和小红同时取款
Thread xiaoming = new Thread(() -> {
System.out.println(account.withdraw("小明", 100000));
});
Thread xiaohong = new Thread(() -> {
System.out.println(account.withdraw("小红", 100000));
});
// 几乎同时启动
xiaoming.start();
xiaohong.start();
xiaoming.join();
xiaohong.join();
// 输出:
// 小明: 取款成功,余额 0
// 小红: 余额不足
}
一句话总结:
对于大多数业务场景(包括银行取款),synchronized代码块是最佳选择——在安全性、性能和可维护性之间取得了最佳平衡。