讲师中心 微信公众号
AI工具推荐 视频效率加速

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

雨瑶小哥_1403

雨瑶小哥_1403

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

|

743人浏览过

|

来源于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 类是核心,它管理账户余额,并负责对存取款操作进行同步。

Java调试
Java调试

通过 JDWP 实现 Java 远程调试:连接远程 JVM、设置断点、单步执行代码、查看变量值、获取调用栈。适用于需要调试远程 Java 应用、排查运行时状态、追踪代码执行流程的场景。 术语注解 JDWP:Java Debug Wire Protocol,Java 调试有线协议(Java 远程调试底层通信协议) remote JVM:远程 Java 虚拟机 step through code:代码单步调试(步入 / 步过 / 步出调试) inspect variables:变量查看 / 变量检视 stac

下载
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;
    }
}

关键点解析:

  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 关键字来保证变量的可见性。但在本例中,`

热门AI工具

更多
小橡皮AI

一款AI工具,主要用于小橡皮一键去AI味,将机器味重的内容改得更有活人感,防止因为 AI 内容被平台打上 AI 标签,以及限流,适合需要提升相关任务效率的用户。

蛙蛙写作

一款AI论文写作工具,主要用于超级AI智能写作助手,适合需要提升相关任务效率的用户。

PixPix
PixPix Hot

PixPix是一款面向电商视觉生产的AI商品图生成工具。

立刻MV
立刻MV Hot

立刻MV是一款AI文本写作工具,AI 音乐视频(MV)创作工具。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

超级简历WonderCV

一款AI办公效率工具,主要用于免费求职简历模版下载制作,应届生职场人必备简历制作神器,适合需要提升相关任务效率的用户。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

墨刀AI
墨刀AI Hot

一款AI图像与设计工具,主要用于产品经理的专属智能体,适合需要提升相关任务效率的用户。

DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

相关专题

更多
while的用法
while的用法

while的用法是“while 条件: 代码块”,条件是一个表达式,当条件为真时,执行代码块,然后再次判断条件是否为真,如果为真则继续执行代码块,直到条件为假为止。本专题为大家提供while相关的文章、下载、课程内容,供大家免费下载体验。

354

2023.09.25

javascriptvoid(o)怎么解决
javascriptvoid(o)怎么解决

javascriptvoid(o)的解决办法:1、检查语法错误;2、确保正确的执行环境;3、检查其他代码的冲突;4、使用事件委托;5、使用其他绑定方式;6、检查外部资源等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

636

2023.11.23

java中void的含义
java中void的含义

本专题整合了Java中void的相关内容,阅读专题下面的文章了解更多详细内容。

371

2025.11.27

c++中volatile关键字的作用
c++中volatile关键字的作用

本专题整合了c++中volatile关键字的相关内容,阅读专题下面的文章了解更多详细内容。

814

2025.10.23

class在c语言中的意思
class在c语言中的意思

在C语言中,"class" 是一个关键字,用于定义一个类。想了解更多class的相关内容,可以阅读本专题下面的文章。

4396

2024.01.03

python中class的含义
python中class的含义

本专题整合了python中class的相关内容,阅读专题下面的文章了解更多详细内容。

326

2025.12.06

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

4638

2023.08.10

Python 多线程与异步编程实战
Python 多线程与异步编程实战

本专题系统讲解 Python 多线程与异步编程的核心概念与实战技巧,包括 threading 模块基础、线程同步机制、GIL 原理、asyncio 异步任务管理、协程与事件循环、任务调度与异常处理。通过实战示例,帮助学习者掌握 如何构建高性能、多任务并发的 Python 应用。

1289

2025.12.24

DBeaver怎么导入数据
DBeaver怎么导入数据

DBeaver数据导入专题,整理Excel、CSV、SQL、TXT等常见文件的数据导入方法,涵盖目标数据库选择、数据表创建、字段映射、数据类型设置及导入参数配置等操作,帮助用户将本地文件中的数据写入数据库,并了解批量导入、重复数据处理和导入失败等常见问题的解决方法。

0

2026.11.05

热门下载

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

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
dev.java 官方:Learn Java
dev.java 官方:Learn Java

共0课时 | 0人学习

Java JDBC数据库连接官方教程
Java JDBC数据库连接官方教程

共0课时 | 0人学习

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

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn