
本文深入探讨了在java中实现多线程共享账户同步的机制,重点讲解如何利用`synchronized`关键字确保并发操作的原子性,并通过`wait()`和`notifyall()`方法有效协调线程间的存取款活动,以维护账户余额的最小和最大限制,从而避免数据不一致和死锁等并发问题。
理解Java多线程与共享资源同步
在多线程编程中,当多个线程尝试访问和修改同一个共享资源(例如本例中的银行账户)时,如果不加以适当的同步控制,就可能导致数据不一致、竞态条件等问题。Java提供了强大的并发工具来解决这些问题,其中最基础且常用的就是synchronized关键字以及Object类的wait()、notify()和notifyAll()方法。
synchronized关键字用于确保同一时间只有一个线程可以执行特定的代码块或方法,从而保护共享资源。而wait()、notify()和notifyAll()则用于线程间的协作,允许线程在特定条件不满足时暂停执行(等待),并在条件满足时被其他线程唤醒。
案例分析:共享银行账户系统
我们将通过一个模拟银行账户存取款的场景来演示这些概念。设有一个银行账户,由两个人(两个线程)共享,他们可以同时进行存款和取款操作。账户有最大余额(500欧元)和最小余额(1欧元)限制。当存款操作会导致余额超过上限,或取款操作会导致余额低于下限时,当前线程需要等待,直到条件允许。
系统包含三个核心类:
立即学习“Java免费学习笔记(深入)”;
- BPA:主程序类,负责创建账户和人物线程。
- Persona:代表操作账户的人,每个Persona对象是一个独立的线程,执行存取款操作。
- Cuenta:代表银行账户,是共享资源,包含余额及存取款的同步逻辑。
实现细节与代码解析
Cuenta 类:账户的同步逻辑
Cuenta类是实现同步机制的关键。它的ingreso(存款)和retiro(取款)方法都必须是synchronized的,以确保对账户余额的操作是原子性的。同时,在这两个方法内部,我们利用wait()和notifyAll()来处理余额限制。
package BPA;
import java.util.Random;
public class Cuenta {
private int saldo; // 当前余额
private final int saldoMax; // 最大余额
private final int saldoMin = 1; // 最小余额
public Cuenta(int saldoInicial, int saldoMaximo) {
this.saldo = saldoInicial;
this.saldoMax = saldoMaximo;
}
/**
* 取款操作
* @param nombre 操作人名称
*/
public synchronized void retiro(String nombre) {
int dinero = new Random().nextInt(350) + 1; // 随机生成取款金额 (1-350)
// 使用while循环判断条件,防止虚假唤醒和条件再次不满足
while ((saldo - dinero) < saldoMin) {
System.out.println(" **** 账户余额不足!" + nombre + " 尝试取出: " + dinero + " 现有余额: " + getSaldo() + " ****");
System.out.println(" ---- " + nombre + " 正在等待存款以进行取款 ---- ");
try {
wait(); // 余额不足,线程等待,并释放Cuenta对象的锁
} catch (InterruptedException e) {
System.out.println(nombre + " 的取款等待被中断。");
Thread.currentThread().interrupt(); // 重新设置中断标志
return; // 退出方法
}
}
// 条件满足,执行取款
this.saldo -= dinero;
System.out.println("Name: " + nombre + " extract cash: " + dinero + " TOTAL CASH: " + getSaldo());
notifyAll(); // 唤醒所有等待在Cuenta对象上的线程,包括可能等待存款的线程
}
/**
* 存款操作
* @param nombre 操作人名称
*/
public synchronized void ingreso(String nombre) {
int dinero = new Random().nextInt(350) + 1; // 随机生成存款金额 (1-350)
// 使用while循环判断条件
while ((saldo + dinero) > saldoMax) {
System.out.println(" **** 账户已达上限!" + nombre + " 尝试存入: " + dinero + " 现有余额: " + getSaldo() + " ****");
System.out.println(" ---- " + nombre + " 正在等待取款以进行存款 ---- ");
try {
wait(); // 余额超上限,线程等待,并释放Cuenta对象的锁
} catch (InterruptedException e) {
System.out.println(nombre + " 的存款等待被中断。");
Thread.currentThread().interrupt(); // 重新设置中断标志
return; // 退出方法
}
}
// 条件满足,执行存款
this.saldo += dinero;
System.out.println("Name: " + nombre + " deposit cash: " + dinero + " TOTAL CASH: " + getSaldo());
notifyAll(); // 唤醒所有等待在Cuenta对象上的线程,包括可能等待取款的线程
}
public int getSaldo() {
return saldo;
}
}关键点说明:
- synchronized 方法: ingreso和retiro方法被synchronized修饰,这意味着任何时候只有一个线程能够进入这些方法,从而保证了对saldo变量的原子性操作。锁定的对象是Cuenta实例本身。
- wait(): 当存取款条件不满足时(例如,取款会导致余额低于saldoMin),线程会调用wait()。这会使当前线程进入等待状态,并释放它持有的Cuenta对象的锁。这样,其他线程就有机会获取锁并执行存取款操作,从而改变账户状态。
- notifyAll(): 在每次成功存入或取出金额后,调用notifyAll()。这会唤醒所有正在等待Cuenta对象锁的线程。这些被唤醒的线程会尝试重新获取锁,并在获取锁后从wait()的地方继续执行。
- while 循环条件判断: wait()方法被唤醒后,线程会重新检查条件是否满足(while ((saldo - dinero)
Persona 类:操作账户的线程
Persona类继承自Thread,每个实例代表一个独立的线程,负责循环调用Cuenta对象的存取款方法。
package BPA;
import java.util.Random;
public class Persona extends Thread {
String nombre;
private Cuenta cuenta;
public Persona(String nombre, Cuenta cuenta) {
this.nombre = nombre;
this.cuenta = cuenta;
}
@Override
public void run() {
while (true) {
// 线程循环进行存取款操作
cuenta.ingreso(nombre);
cuenta.retiro(nombre);
try {
// 模拟操作间隔,避免CPU空转过快,同时给其他线程机会
Thread.sleep(new Random().nextInt(500) + 500); // 随机暂停0.5到1.0秒
} catch (InterruptedException e) {
System.out.println(nombre + " 的操作被中断。");
Thread.currentThread().interrupt(); // 重新设置中断标志
break; // 退出循环
}
}
}
}关键点说明:
- Persona线程直接调用Cuenta对象的ingreso和retiro方法。由于这些方法本身已经包含了同步逻辑,Persona类的run()方法不需要额外的synchronized块或wait()/notifyAll()调用来管理账户同步。
- Thread.sleep():在每次存取款操作后,线程会暂停一段时间。这有助于模拟真实世界的延迟,并让其他线程有机会运行,避免某个线程过度占用CPU。
BPA 类:主程序入口
BPA类是程序的入口点,负责创建Cuenta对象和Persona线程,并启动它们。
package BPA;
public class BPA {
public static void main(String[] args) {
// 创建一个初始余额为40,最大余额为500的账户
Cuenta laCuenta = new Cuenta(40, 500);
// 创建两个操作人线程,并关联到同一个账户
Persona Ramon = new Persona("Ramon", laCuenta);
Persona Quique = new Persona("Quique", laCuenta);
// 启动线程
Quique.start();
Ramon.start();
try {
// 等待两个线程执行完毕(尽管在本例中它们会无限循环,除非被中断)
Quique.join();
Ramon.join();
} catch (InterruptedException ex) {
System.out.println("主线程被中断。");
}
}
}关键点说明:
- join()方法:main线程调用Quique.join()和Ramon.join()会使main线程等待这两个Persona线程执行完毕。在我们的例子中,Persona线程是无限循环的,所以main线程会一直等待,直到程序被手动终止或线程被中断。
正确实现的关键点与注意事项
- wait()和notifyAll()的调用位置: 它们必须在synchronized块或synchronized方法内部调用,并且必须在当前线程持有该对象监视器(锁)的情况下调用。否则,会抛出IllegalMonitorStateException。
- wait()释放锁: wait()方法会释放当前线程持有的对象锁,并进入等待状态。这是实现线程协作的关键,允许其他线程有机会获取锁并修改共享资源。
-
notify() vs notifyAll():
- notify():随机唤醒一个等待在该对象上的线程。
- notifyAll():唤醒所有等待在该对象上的线程。在多生产者-多消费者或多存多取场景中,通常推荐使用notifyAll(),以确保所有相关线程都有机会检查条件并继续执行,避免潜在的死锁或效率问题。
- 条件判断使用while循环: 永远不要用if语句来检查wait()后的条件。线程可能被虚假唤醒,或者在被唤醒时,由于其他线程的操作,条件再次不满足。while循环能确保线程只有在条件真正满足时才继续执行。
- 处理InterruptedException: wait()和sleep()方法都可能抛出InterruptedException。在捕获此异常时,最佳实践是重新设置线程的中断状态(Thread.currentThread().interrupt()),以便调用栈上层的代码能够感知到中断,并根据需要进行处理。
总结
通过本教程,我们学习了如何在Java中利用synchronized关键字、wait()和notifyAll()方法来有效地管理多线程对共享资源的并发访问。这种模式在处理诸如生产者-消费者问题、线程池等需要线程协作的场景中非常常见。理解这些并发原语的工作机制对于编写健壮、高效的Java多线程










