首页/新闻资讯/正文详情

Java 线程安全技术笔记:以银行存取款为例

发布时间:2026/9/26 10:09:03 来源:云帆数科 栏目:资讯中心
Java 线程安全技术笔记:以银行存取款为例
前言在我们开发关于程序的时候并发编程表面上同时进行实际上是cpu快速切换线程执行这就会产生线程安全问题多线程程序中多个线程可能同时操作同一份数据。要保证结果正确就需要理解共享数据、原子操作以及线程同步。本篇通过银行存取款的例子分析线程安全问题的产生原因和解决方法。1.什么是线程安全线程安全是指多个线程并发访问共享数据时无论执行顺序如何交错程序都能保持正确的结果。例如一个账户有 1000 元两个线程分别尝试取出 800 元。正确结果应该是一个线程取款成功。另一个线程因为余额不足而失败。最终余额为 200 元。如果两个线程都取款成功就说明程序没有正确协调对账户余额的访问。2.线程安全问题是怎样产生的publicvoidwithdraw(longmoney){if(balancemoney){balance-money;}}注意判断余额”和“扣款”是两个步骤线程执行到中间时其他线程可能插入操作账户余额为1000元线程A检查余额1000 800允许取款线程B检查余额1000 800允许取款线程A执行扣款线程B也执行扣款另外balance - money 本身也不是原子操作原子操作就是一个不可分割的操作其他线程不会观察到它“只完成了一半”的中间状态。它包含读取余额 → 计算新余额 → 写回余额因此实际错误结果可能随着线程执行顺序而变化。一行代码不一定是一个原子操作。count、count-- 同样包含读取、计算和写回的过程。3.使用 synchronized 保证同步synchronized 可以让使用同一把锁的线程互斥地执行同步代码。publicsynchronizedbooleanwithdraw(longmoney){if(money0){thrownewIllegalArgumentException(取款金额必须大于0);}if(balancemoney){returnfalse;}balance-money;returntrue;}线程进入这个实例同步方法之前需要先获得当前账户对象的锁。线程A获得账户锁-线程A检查余额并完成扣款-线程A退出同步方法释放-线程B获得账户锁-线程B检查扣款后的余额#需要注意synchronized 不保证线程不会被调度暂停。它保证的是线程持有锁时其他需要获取同一把锁的线程不能进入对应的同步区域。除了互斥同一把锁的释放和后续获取还提供了可见性保证使后续获得锁的线程能够看到之前的修改。4. 锁对象必须一致synchronized (obj) { … }, obj 指向的对象.实例方法加 synchronized, 当前实例 this静态方法加 synchronized, 所属类的 Class 对象在银行案例中多个任务必须共享同一个账户AccountaccountnewAccount(100_000);WithdrawTasktask1newWithdrawTask(account,80_000);WithdrawTasktask2newWithdrawTask(account,80_000);DepositTasktask3newDepositTask(account,50_000);存款和取款方法都定义在 Account 中并使用实例同步方法因此锁住的是同一个 account.如果把锁加在不同任务对象的 run() 方法上publicsynchronizedvoidrun(){// 操作共享账户}锁住的就是各自的任务对象。不同任务对象拥有不同的锁仅靠这些锁无法保护共享账户。5.同步范围要覆盖完整业务操作if(account.getBalance()money){account.deduct(money);}检查余额 → 判断能否取款 → 扣减余额.放在同一个同步方法或同步代码块中。6.银行案例的类职责1.Account:保存余额提供同步的存款、取款和查询方法publicclassAccount{privateintbalance;publicAccount(intinitialBalance){if(initialBalance0){thrownewIllegalArgumentException(初始余额不能为负数);}balanceinitialBalance;}2.DepositTask:实现 Runnable在 run() 中执行存款publicDepositTask(Accountaccount,intmoney){this.accountaccount;this.moneymoney;}publicvoidrun(){account.deposit(money);}}3.WithdrawTask:实现 Runnable在 run() 中执行取款publicWithdrawTask(Accountaccount,intmoney){this.accountaccount;this.moneymoney;}publicvoidrun(){account.withdraw(money);}}4.BankDemo:创建共享账户、任务和线程并启动程序WithdrawTasktask1newWithdrawTask(account,200);WithdrawTasktask2newWithdrawTask(account,300);DepositTasktask3newDepositTask(account,800);Threadthread1newThread(task1,取款线程1);Threadthread2newThread(task2,取款线程2);Threadthread3newThread(task3,存款线程3);thread1.start();thread2.start();thread3.start();thread1.join();thread2.join();thread3.join();7.run() 和 start() 的区别1.run() 定义任务内容start() 启动新线程。Threadt1newThread(task1,取款线程A);t1.start();调用 start() 后新线程会执行对应任务的 run()主线程可以继续向下执行。如果直接调用t1.run();对于这里创建的 Thread任务会由当前调用线程执行不会启动新线程。如果调用者是主线程取款操作就在主线程中完成。注意如果只是重复调用run的话就不叫并发操作了就是主线程处理一个任务。8.join() 的作用启动线程后主线程不会自动等待它们完成。如果立刻读取余额存取款任务可能还没有结束。t1.start();t2.start();t3.start();t1.join();t2.join();t3.join();System.out.println(account.getBalance());t1.join() 表示当前调用线程等待 t1 执行结束。这里等待的是主线程其他工作线程仍然可以继续执行。三个 join() 正常返回后再读取最终余额。join() 用于等待任务完成synchronized 用于协调共享数据访问两者作用不同。9. 阻塞与等待状态其中容易混淆的是BLOCKED例如账户锁被其他线程持有当前线程等待获得锁。WAITING例如调用无超时的 join()等待目标线程结束。TIMED_WAITING例如调用 Thread.sleep(1000)休眠约一秒。等待锁不等于等待持锁线程结束。持锁线程退出同步代码块时就可以释放锁然后继续执行其他代码。另外sleep() 不释放已持有的监视器锁wait() 会释放所等待对象的监视器锁并在返回前重新获得它。10.整体代码publicclassAccount{privateintbalance;publicAccount(intinitialBalance){if(initialBalance0){thrownewIllegalArgumentException(初始余额不能为负数);}balanceinitialBalance;}publicsynchronizedvoiddeposit(intmoney){if(money0){thrownewIllegalArgumentException(余额不能为负数);}balanceMath.addExact(balance,money);//超出int 范围System.out.println(Thread.currentThread().getName()存入:money分,余额:balance分);}//取款方法publicsynchronizedbooleanwithdraw(intmoney){if(money0){thrownewIllegalArgumentException(取款必须大于0);}if(balancemoney){System.out.println(Thread.currentThread().getName()余额不足取款失败,当前余额:分);returnfalse;}balance-money;System.out.println(Thread.currentThread().getName() 取出money分余额balance分);returntrue;}publicclassDepositTaskimplementsRunnable{privatefinalAccountaccount;privatefinalintmoney;publicDepositTask(Accountaccount,intmoney){this.accountaccount;this.moneymoney;}publicvoidrun(){account.deposit(money);}}publicclassWithdrawTaskimplementsRunnable{privatefinalAccountaccount;privatefinalintmoney;publicWithdrawTask(Accountaccount,intmoney){this.accountaccount;this.moneymoney;}publicvoidrun(){account.withdraw(money);}}publicclassBankDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{AccountaccountnewAccount(1000);//取出800WithdrawTasktask1newWithdrawTask(account,200);WithdrawTasktask2newWithdrawTask(account,300);DepositTasktask3newDepositTask(account,800);Threadthread1newThread(task1,取款线程1);Threadthread2newThread(task2,取款线程2);Threadthread3newThread(task3,存款线程3);thread1.start();thread2.start();thread3.start();thread1.join();thread2.join();thread3.join();System.out.println(最终余额account.getBalance()分);}}11.编写共享数据的并发代码时应明确三个问题**哪些数据被共享、用哪把锁保护、哪些步骤必须作为整体执行。**银行存取款中的余额、账户锁以及“检查余额并扣款”正好对应这三个问题。

相关推荐

10G SFP+光模块选型避坑指南:链路预算与兼容性实战解析
10G SFP+光模块选型避坑指南:链路预算与兼容性实战解析

1. 为什么10G SFP光模块选型不是“插上就能用”的简单事 你手头刚上了一台新采购的万兆交换机,配套的SFP光模块随手一插——链路灯亮了,ping通了,网管里端口状态显示UP。你松了口气,觉得这事就算搞定了。结果两周后,业… · 2026/9/26 10:09:03

基于springboot的景区景点推荐导览系统的毕业设计与实现
基于springboot的景区景点推荐导览系统的毕业设计与实现

学弟学妹们,大家好👋!作为计算机专业已经毕业多年的学长,回头再看毕业设计这段日子,依旧感慨万千🌟。当年坐在电脑前,一点点调试接口、逐字打磨论文的画面,现在想起来还历历在目。看… · 2026/9/26 10:09:03

WPF MVVM代码骨架:从绑定刷新到异步命令的避坑指南
WPF MVVM代码骨架:从绑定刷新到异步命令的避坑指南

简介:面向C#/WPF开发者的MVVM模式实战项目,以完整可运行的WPF应用演示Model-View-ViewModel三层解耦、数据绑定、命令处理、依赖属性等核心机制,适合希望从理论过渡到实际项目的初中级桌面开发人员。压缩包共141个文件,包含44个cs… · 2026/9/26 10:09:03

GitHub Copilot X 编程助手配 TaoToken:settings.json 骨架与报错排查
GitHub Copilot X 编程助手配 TaoToken:settings.json 骨架与报错排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 10:50:48

CTF夺旗赛入门指南:从环境搭建到Web、逆向、Pwn实战
CTF夺旗赛入门指南:从环境搭建到Web、逆向、Pwn实战

1. 从零认识CTF夺旗赛:它到底是什么,为什么值得投入很多人第一次听到CTF这三个字母,脑子里浮现的是“黑客”“攻防”“高深莫测”这类词,觉得离自己很远。其实CTF(Capture The Flag,夺旗赛)本质… · 2026/9/26 10:50:48

多Agent-A2A协作入门指南:三大角色+四大对象,全面掌握Agent协作必备知识(建议收藏)
多Agent-A2A协作入门指南:三大角色+四大对象,全面掌握Agent协作必备知识(建议收藏)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 10:50:48

2026年云上及Windows本地部署OpenClaw(Clawdbot) 集成skill保姆级教程:TaoToken统一Key接入与config.toml配置实战
2026年云上及Windows本地部署OpenClaw(Clawdbot) 集成skill保姆级教程:TaoToken统一Key接入与config.toml配置实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 10:50:47

金山对雅虎助手的测试报告:用 TaoToken 统一 Key 跑通 settings.json 配置骨架
金山对雅虎助手的测试报告:用 TaoToken 统一 Key 跑通 settings.json 配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 10:50:47

STM32开源项目深度解析:代码+原理图+仿真三位一体工程实践
STM32开源项目深度解析:代码+原理图+仿真三位一体工程实践

1. 为什么这个STM32开源项目值得你花时间细看——不是所有“带代码原理图仿真”的都叫真开源最近在几个嵌入式技术社区刷到一个标题很朴实的项目:“STM32项目开源:评价(代码 原理图 仿真)”。没加任何修饰词,没蹭“爆… · 2026/9/26 10:50:41

数据库课后习题答案别硬背:当测试用例集刷,效率翻倍
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍

简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21

OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40

向下兼容与向上兼容:接口设计中的兼容性策略与工程实践
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践

一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码