0

0

Java多线程并发:实现共享账户的同步存取

霞舞

霞舞

发布时间:2025-11-25 21:52:01

|

723人浏览过

|

来源于php中文网

原创

Java多线程并发:实现共享账户的同步存取

本文深入探讨了在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) < saldoMin) {
            System.out.println("  ****  账户余额不足! " + 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(); // 通知所有等待的线程(包括存款线程和取款线程),账户状态已改变
    }

    /**
     * 存款操作
     * @param nombre 执行操作的人员名称
     * @param cuenta 当前账户实例 (这里可以省略,因为方法内部直接使用this)
     */
    public synchronized void ingreso(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;
    }
}

关键点解析:

Runway
Runway

Runway是一个AI创意工具平台,它提供了一系列强大的功能,旨在帮助用户在视觉内容创作、设计和开发过程中提高效率和创新能力。

下载
  1. synchronized 方法: retiro 和 ingreso 方法都被synchronized修饰,这意味着在任何给定时间,只有一个线程可以执行这两个方法中的任意一个,从而保证了对saldo变量的原子操作。锁是Cuenta对象实例本身。
  2. while 循环检查条件: wait()方法被唤醒后,线程会重新尝试获取锁。获取锁后,它不会从wait()的下一行直接执行,而是会重新检查导致它等待的条件。使用while循环而不是if语句来检查条件是至关重要的,这可以防止“虚假唤醒”(spurious wakeup)以及在条件未真正满足时线程继续执行。
  3. wait() 释放锁: 当账户余额不足以取款或存款将超出最大限制时,线程调用wait()。这会使当前线程进入等待状态,并释放Cuenta对象的监视器锁,允许其他线程(例如另一个存款线程或取款线程)获取锁并修改账户状态。
  4. 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; // 退出循环
            }
        }
    }
}

关键点解析:

  1. 线程职责分离: Persona 线程只负责调用 Cuenta 对象的存取款方法,而不直接处理同步逻辑。所有同步和等待/通知机制都封装在 Cuenta 类中。
  2. 无 synchronized 在 run() 方法: Persona 的 run() 方法不需要synchronized,因为锁是在 Cuenta 对象的方法中获取的。在 Persona 内部进行同步将是错误的,因为它会锁定 Persona 实例本身,而不是共享的 Cuenta 实例。
  3. 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. 注意事项与最佳实践

  1. wait() 必须在 while 循环中: 始终使用 while (condition) 循环来检查 wait() 的条件,而不是 if (condition)。这是为了处理虚假唤醒和条件在被唤醒后仍未满足的情况。
  2. wait() 和 notify()/notifyAll() 必须在 synchronized 块/方法内: 它们只能在持有对象监视器锁的情况下调用,否则会抛出 IllegalMonitorStateException。
  3. 选择 notify() 还是 notifyAll():
    • notify() 随机唤醒一个等待线程。如果所有等待线程都在等待相同的条件,或者你知道哪个线程应该被唤醒,可以使用它。
    • notifyAll() 唤醒所有等待线程。这通常是更安全的选择,因为它避免了“死锁”情况,即错误的线程被唤醒而正确的线程仍然在等待。在本例中,存款和取款线程可能互相等待,因此 notifyAll() 是合适的。
  4. 避免死锁: 精心设计同步块和锁的获取顺序,以避免线程互相等待对方释放资源。
  5. 中断处理: 当线程在 wait() 状态下被中断时,会抛出 InterruptedException。最佳实践是在捕获此异常后,通过 Thread.currentThread().interrupt() 重新设置中断标志,并决定线程是应该继续执行还是终止。
  6. volatile 关键字 (补充): 如果共享变量只涉及可见性问题(一个线程修改,另一个线程读取),且不涉及复合操作(读-改-写),那么可以使用 volatile 关键字来保证变量的可见性。但在本例中,`

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

837

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

741

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

737

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

397

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

399

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

446

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

430

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16926

2023.08.03

PHP WebSocket 实时通信开发
PHP WebSocket 实时通信开发

本专题系统讲解 PHP 在实时通信与长连接场景中的应用实践,涵盖 WebSocket 协议原理、服务端连接管理、消息推送机制、心跳检测、断线重连以及与前端的实时交互实现。通过聊天系统、实时通知等案例,帮助开发者掌握 使用 PHP 构建实时通信与推送服务的完整开发流程,适用于即时消息与高互动性应用场景。

11

2026.01.19

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 2.7万人学习

C# 教程
C# 教程

共94课时 | 7万人学习

Java 教程
Java 教程

共578课时 | 47.9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号