《疯狂java讲义》学习(44):线程同步

2024-04-17 20:48

本文主要是介绍《疯狂java讲义》学习(44):线程同步,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1线程同步

多线程编程是有趣的事情,它很容易突然出现“错误情况”,这是有系统的线程调度具有一定的随机性造成的,不过即使程序偶然出现问题,那也是由于编程不当引起的。当使用多个线程来访问同一个数据时,很容易“偶然”出现线程安全问题。

1.1线程安全问题

关于线程安全问题,有一个经典的问题——银行取钱的问题。银行取钱的基本流程基本上可以分为如下几个步骤。

  1. 用户数据账户、密码,系统判断用户的账户、密码是否匹配。
  2. 用户输入取款金额。
  3. 系统判断账户金额是否大于取款金额。
  4. 如果金额大于取款金额,则取款成功;如果金额小于取款金额,则取款失败。

乍一看上去,这个流程确实就是我们日常生活中的取款流程,这个流程没有任何问题。但一旦将这个流程放在多线程并发的场景下,就有可能出现问题。注意此处说的是有可能,并不是说一定。也许你的程序运行了一百万次都没有出现问题,但没有出现问题并不等于没有问题!
按上面的流程去编写取款程序,而且我么使用两个线程来模拟取钱操作,模拟两个人使用同一个账户并发取钱的问题。我们不管检查账户和密码的操作,仅仅模拟后面三步操作。下面先定义一个账户类,该账户类封装了账户编号和余额两个属性。

package Account;public class Account {// 封装账户编号、账户余额两个Fieldprivate String accountNo;private double balance;public Account() { }// 构造器public Account(String accountNo, double balance) {this.accountNo = accountNo;this.balance = balance;}public String getAccountNo(){return this.accountNo;}public void setAccountNo(String accountNo) {this.accountNo = accountNo;}public double getBalance(){return this.balance;}public void getBalance(double balance) {this.balance = balance;}// 下面方法根据accountNo来重写hashCode()和equals()方法public int hashCode() {return accountNo.hashCode();}public boolean equals(Object obj) {if(this == obj)return true;if (obj != null && obj.getClass()==Account.class) {Account target = (Account)obj;return target.getAccountNo().equals(accountNo);}return false;}
}

接下来提供一个取钱的线程类,该线程类根据执行账户、取钱数量进行取钱操作,取钱的逻辑是当其余额不足时无法提取现金,当余额足够时系统突出钞票,余额减少。

package Account;public class DrawThread extends Thread {//模拟用户账户private Account account;// 当前取钱线程所希望取的钱数private double drawAmount;public DrawThread(String name, Account account, double drawAmount) {super(name);this.account = account;this.drawAmount = drawAmount;}// 当多个线程修改用一个共享数据时,将涉及数据安全问题public void run() {// 账户金额大于取钱数目if (account.getBalance() >= drawAmount) {// 突出钞票System.out.println(getName() + "取钱成功!取出钞票" + drawAmount);try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}account.setBalance(account.getBalance() - drawAmount);System.out.println("\t余额为:" + account.getBalance());} else {System.out.println(getName()+"取钱失败!余额不足!");}}
}

读者先不要管程序中那段被注释掉的粗体字代码,上面程序是一个非常简单的取钱逻辑,这个取钱逻辑与实际的取钱操作也很相似。程序的主程序非常简单,仅仅是创建一个账户,并启动两个线程从该账户中取钱,程序如下:

package Account;public class DrawTest {public static void main(String[] args) {// 创建一个账户Account acct = new Account("1234567", 1000);// 模拟两个线程对用一个账号取钱new DrawThread("ffzs", acct, 800).start();new DrawThread("sleepycat", acct, 800).start();}
}

多次运行上面程序,有可能会出现如下结果:

ffzs取钱成功!取出钞票800.0
sleepycat取钱成功!取出钞票800.0余额为:200.0余额为:-600.0

运行结果并不是我们所期望的结果(不过也有可能看到运行正确的效果),这正是多线程编程突然出现的“偶然”错误——因为线程调度的不确定性。假设系统线程调度器在粗体字代码处暂停,让另一个线程执行——为了强制暂停,只要取消上面程序中粗体字代码的注释即可。取消注释后再次编译DrawThread.java,并再次运行DrawTest类,将总可以看到上面的结果。
问题出现了:账户余额只有1000时取出了1600,而且账户余额出现了负值,这不是银行希望的结果。虽然上面程序是人为地使用Thread.sleep(1)来强制线程调度切换,但这种切换也是完全可能发生的——100000次操作只要有1次出现了错误,那就是编程错误引起的。

1.2同步代码块

出现取款问题的原因是run()方法的方法体不具备同步安全性——程序中有两个并发线程在修改Account对象;而且系统恰好在粗体字代码出执行线程切换,切换给另一个修改Account对象的线程,所以就出现了问题。
为了解决这个问题,Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式如下:

synchronized(obj)
{...//此处的代码就是同步代码块
}

上面语法格式中synchronized后括号里的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。

任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。

虽然Java程序允许使用任何对象作为同步监视器,但想一下同步监视器的目的:阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。对于上面的取钱模拟程序,我们应该考虑使用账户(account)作为同步监视器。我们把程序修改成如下形式:

package Account;public class DrawThread extends Thread {//模拟用户账户private Account account;// 当前取钱线程所希望取的钱数private double drawAmount;public DrawThread(String name, Account account, double drawAmount) {super(name);this.account = account;this.drawAmount = drawAmount;}// 当多个线程修改用一个共享数据时,将涉及数据安全问题public void run() {// 使用account作为同步监视器,任何线程进入下面同步代码块之前// 必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它// 这种做法符合:“加锁->修改->释放锁”的逻辑synchronized (account) {// 账户金额大于取钱数目if (account.getBalance() >= drawAmount) {// 突出钞票System.out.println(getName() + "取钱成功!取出钞票" + drawAmount);try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTr

这篇关于《疯狂java讲义》学习(44):线程同步的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/912814

相关文章

Java实现删除文件中的指定内容

《Java实现删除文件中的指定内容》在日常开发中,经常需要对文本文件进行批量处理,其中,删除文件中指定内容是最常见的需求之一,下面我们就来看看如何使用java实现删除文件中的指定内容吧... 目录1. 项目背景详细介绍2. 项目需求详细介绍2.1 功能需求2.2 非功能需求3. 相关技术详细介绍3.1 Ja

springboot项目中整合高德地图的实践

《springboot项目中整合高德地图的实践》:本文主要介绍springboot项目中整合高德地图的实践,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一:高德开放平台的使用二:创建数据库(我是用的是mysql)三:Springboot所需的依赖(根据你的需求再

spring中的ImportSelector接口示例详解

《spring中的ImportSelector接口示例详解》Spring的ImportSelector接口用于动态选择配置类,实现条件化和模块化配置,关键方法selectImports根据注解信息返回... 目录一、核心作用二、关键方法三、扩展功能四、使用示例五、工作原理六、应用场景七、自定义实现Impor

SpringBoot3应用中集成和使用Spring Retry的实践记录

《SpringBoot3应用中集成和使用SpringRetry的实践记录》SpringRetry为SpringBoot3提供重试机制,支持注解和编程式两种方式,可配置重试策略与监听器,适用于临时性故... 目录1. 简介2. 环境准备3. 使用方式3.1 注解方式 基础使用自定义重试策略失败恢复机制注意事项

SpringBoot整合Flowable实现工作流的详细流程

《SpringBoot整合Flowable实现工作流的详细流程》Flowable是一个使用Java编写的轻量级业务流程引擎,Flowable流程引擎可用于部署BPMN2.0流程定义,创建这些流程定义的... 目录1、流程引擎介绍2、创建项目3、画流程图4、开发接口4.1 Java 类梳理4.2 查看流程图4

一文详解如何在idea中快速搭建一个Spring Boot项目

《一文详解如何在idea中快速搭建一个SpringBoot项目》IntelliJIDEA作为Java开发者的‌首选IDE‌,深度集成SpringBoot支持,可一键生成项目骨架、智能配置依赖,这篇文... 目录前言1、创建项目名称2、勾选需要的依赖3、在setting中检查maven4、编写数据源5、开启热

Java对异常的认识与异常的处理小结

《Java对异常的认识与异常的处理小结》Java程序在运行时可能出现的错误或非正常情况称为异常,下面给大家介绍Java对异常的认识与异常的处理,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参... 目录一、认识异常与异常类型。二、异常的处理三、总结 一、认识异常与异常类型。(1)简单定义-什么是

SpringBoot项目配置logback-spring.xml屏蔽特定路径的日志

《SpringBoot项目配置logback-spring.xml屏蔽特定路径的日志》在SpringBoot项目中,使用logback-spring.xml配置屏蔽特定路径的日志有两种常用方式,文中的... 目录方案一:基础配置(直接关闭目标路径日志)方案二:结合 Spring Profile 按环境屏蔽关

Java使用HttpClient实现图片下载与本地保存功能

《Java使用HttpClient实现图片下载与本地保存功能》在当今数字化时代,网络资源的获取与处理已成为软件开发中的常见需求,其中,图片作为网络上最常见的资源之一,其下载与保存功能在许多应用场景中都... 目录引言一、Apache HttpClient简介二、技术栈与环境准备三、实现图片下载与保存功能1.

canal实现mysql数据同步的详细过程

《canal实现mysql数据同步的详细过程》:本文主要介绍canal实现mysql数据同步的详细过程,本文通过实例图文相结合给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的... 目录1、canal下载2、mysql同步用户创建和授权3、canal admin安装和启动4、canal