
本文深入探讨了在java多线程环境下,如何安全地管理共享资源(如银行账户)的并发访问。通过详细分析synchronized、wait()和notifyall()机制,我们展示了如何确保多线程对账户进行存取操作时的原子性和一致性,避免数据竞争和死锁,从而实现一个健壮的并发控制模型。
在现代应用程序开发中,多线程并发编程是提高系统性能和响应能力的关键技术。然而,当多个线程同时访问和修改共享资源时,如果没有适当的同步机制,就可能导致数据不一致、竞态条件甚至死锁等问题。本文将以一个经典的银行账户存取场景为例,详细讲解如何在Java中利用synchronized、wait()和notifyAll()方法实现线程间的协作与同步,确保共享账户的安全操作。
1. 并发编程中的共享资源挑战
设想一个银行账户,由多个人(对应多个线程)同时进行存款(deposit)和取款(extract)操作。如果不对这些操作进行同步,可能会出现以下问题:
竞态条件 (Race Condition): 两个线程同时读取账户余额,一个线程进行取款,另一个进行存款,最终结果可能与预期不符。数据不一致: 账户余额可能因为并发操作而出现错误的值。死锁 (Deadlock): 线程之间互相等待对方释放资源,导致所有线程都无法继续执行。
为了解决这些问题,我们需要引入同步机制来保证在某一时刻,只有一个线程能够访问和修改共享资源的关键部分。
2. Java同步机制核心:synchronized、wait() 和 notifyAll()
Java提供了内置的同步机制,主要通过以下关键字和方法实现:
立即学习“Java免费学习笔记(深入)”;
synchronized 关键字:作用: 用于修饰方法或代码块,确保在同一时间只有一个线程可以执行被synchronized修饰的代码。它本质上是获取对象的监视器锁(monitor lock)。用法:同步方法: public synchronized void methodName() { … } 锁是当前实例对象(this)。同步静态方法: public static synchronized void staticMethodName() { … } 锁是当前类的Class对象。同步代码块: synchronized (lockObject) { … } 锁是指定的lockObject对象。wait() 方法:作用: 当一个线程持有对象的监视器锁,但它需要等待某个条件满足才能继续执行时,可以调用wait()方法。调用wait()会立即释放当前线程持有的锁,并进入等待状态,直到被notify()或notifyAll()唤醒。注意: wait()必须在synchronized块或方法内部调用,否则会抛出IllegalMonitorStateException。notify() / notifyAll() 方法:作用: 用于唤醒在同一个对象上调用wait()方法而等待的线程。notify(): 随机唤醒一个等待的线程。notifyAll(): 唤醒所有等待的线程。注意: notify()和notifyAll()也必须在synchronized块或方法内部调用,否则会抛出IllegalMonitorStateException。被唤醒的线程不会立即执行,而是会尝试重新获取对象的监视器锁。
3. 账户存取同步实现
我们将通过三个类来实现这个银行账户存取系统:
BPA: 主应用程序类,负责创建账户和人员线程。Cuenta: 银行账户类,作为共享资源,包含存款和取款的同步逻辑。Persona: 人员类,作为线程,模拟存取款操作。
3.1 Cuenta 类:共享账户与同步逻辑
Cuenta 类是核心,它管理账户余额,并负责对存取款操作进行同步。
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 执行操作的人员名称 * @param cuenta 当前账户实例 (这里可以省略,因为方法内部直接使用this) */ public synchronized void retiro(String nombre) { int dinero = new Random().nextInt(350) + 1; // 随机生成取款金额 (1-350) // 使用while循环检查条件,防止虚假唤醒 while ((saldo - dinero) saldoMax) { System.out.println(" **** 账户已达最大容量 " + saldoMax + " €。 " + nombre + " 尝试存入: " + dinero + " €,当前余额: " + getSaldo() + " € ****"); System.out.println(" ---- " + nombre + " 正在等待取款以进行存款 ---- "); try { wait(); // 余额超出上限,释放锁并等待 } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 恢复中断状态 System.out.println(nombre + " 的存款操作被中断。"); return; // 退出方法 } // 唤醒后重新生成金额 dinero = new Random().nextInt(350) + 1; } // 条件满足,执行存款 this.saldo += dinero; System.out.println("Name: " + nombre + " 存入: " + dinero + " €,当前余额: " + getSaldo() + " €"); notifyAll(); // 通知所有等待的线程,账户状态已改变 } public int getSaldo() { return saldo; }}
关键点解析:
绘蛙AI修图
绘蛙平台AI修图工具,支持手脚修复、商品重绘、AI扩图、AI换色
285 查看详情
synchronized 方法: retiro 和 ingreso 方法都被synchronized修饰,这意味着在任何给定时间,只有一个线程可以执行这两个方法中的任意一个,从而保证了对saldo变量的原子操作。锁是Cuenta对象实例本身。while 循环检查条件: wait()方法被唤醒后,线程会重新尝试获取锁。获取锁后,它不会从wait()的下一行直接执行,而是会重新检查导致它等待的条件。使用while循环而不是if语句来检查条件是至关重要的,这可以防止“虚假唤醒”(spurious wakeup)以及在条件未真正满足时线程继续执行。wait() 释放锁: 当账户余额不足以取款或存款将超出最大限制时,线程调用wait()。这会使当前线程进入等待状态,并释放Cuenta对象的监视器锁,允许其他线程(例如另一个存款线程或取款线程)获取锁并修改账户状态。notifyAll() 唤醒线程: 每当成功进行存款或取款操作后,账户状态发生改变,调用notifyAll()来唤醒所有在Cuenta对象上等待的线程。这些被唤醒的线程会尝试重新获取锁,并在获得锁后重新检查它们的等待条件。
3.2 Persona 类:人员线程
Persona 类代表一个操作银行账户的人,它是一个线程。
package BPA;public class Persona extends Thread { String nombre; private final 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 { // 模拟操作间隔,让其他线程有机会执行 Thread.sleep(new Random().nextInt(1000) + 500); // 随机休眠500ms到1500ms } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.out.println(nombre + " 线程被中断,停止运行。"); break; // 退出循环 } } }}
关键点解析:
线程职责分离: Persona 线程只负责调用 Cuenta 对象的存取款方法,而不直接处理同步逻辑。所有同步和等待/通知机制都封装在 Cuenta 类中。无 synchronized 在 run() 方法: Persona 的 run() 方法不需要synchronized,因为锁是在 Cuenta 对象的方法中获取的。在 Persona 内部进行同步将是错误的,因为它会锁定 Persona 实例本身,而不是共享的 Cuenta 实例。Thread.sleep(): 用于模拟每次操作之间的时间间隔,使得输出更易于观察,并让CPU有机会调度其他线程。
3.3 BPA 类:主应用程序
BPA 类是应用程序的入口点。
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(); // 理论上,如果线程有终止条件,这里可以使用join()等待它们结束 // 但在本例中,线程是无限循环运行的,除非被中断。 // try { // Quique.join(); // Ramon.join(); // } catch (InterruptedException ex) { // System.out.println("主线程被中断。"); // } }}
4. 运行与观察
运行 BPA 类,你将看到两个人员线程(Ramon 和 Quique)不断地对同一个账户进行存取款操作。当账户余额不足以取款或存款会超出上限时,相应的线程会进入等待状态,直到另一个线程通过存取款操作改变了账户状态并调用notifyAll()将其唤醒。
例如,输出可能包含:
Name: Quique 存入: 150 €,当前余额: 190 €Name: Ramon 取出: 80 €,当前余额: 110 €Name: Quique 存入: 200 €,当前余额: 310 € **** 账户已达最大容量 500 €。 Ramon 尝试存入: 250 €,当前余额: 310 € **** ---- Ramon 正在等待取款以进行存款 ---- Name: Quique 取出: 100 €,当前余额: 210 €Name: Ramon 存入: 250 €,当前余额: 460 €
这表明了线程间的协作和同步是成功的。
5. 注意事项与最佳实践
wait() 必须在 while 循环中: 始终使用 while (condition) 循环来检查 wait() 的条件,而不是 if (condition)。这是为了处理虚假唤醒和条件在被唤醒后仍未满足的情况。wait() 和 notify()/notifyAll() 必须在 synchronized 块/方法内: 它们只能在持有对象监视器锁的情况下调用,否则会抛出 IllegalMonitorStateException。选择 notify() 还是 notifyAll():notify() 随机唤醒一个等待线程。如果所有等待线程都在等待相同的条件,或者你知道哪个线程应该被唤醒,可以使用它。notifyAll() 唤醒所有等待线程。这通常是更安全的选择,因为它避免了“死锁”情况,即错误的线程被唤醒而正确的线程仍然在等待。在本例中,存款和取款线程可能互相等待,因此 notifyAll() 是合适的。避免死锁: 精心设计同步块和锁的获取顺序,以避免线程互相等待对方释放资源。中断处理: 当线程在 wait() 状态下被中断时,会抛出 InterruptedException。最佳实践是在捕获此异常后,通过 Thread.currentThread().interrupt() 重新设置中断标志,并决定线程是应该继续执行还是终止。volatile 关键字 (补充): 如果共享变量只涉及可见性问题(一个线程修改,另一个线程读取),且不涉及复合操作(读-改-写),那么可以使用 volatile 关键字来保证变量的可见性。但在本例中,`
以上就是Java多线程并发:实现共享账户的同步存取的详细内容,更多请关注创想鸟其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1075938.html
微信扫一扫
支付宝扫一扫