《疯狂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 NoClassDefFoundError运行时错误分析解决

《JavaNoClassDefFoundError运行时错误分析解决》在Java开发中,NoClassDefFoundError是一种常见的运行时错误,它通常表明Java虚拟机在尝试加载一个类时未能... 目录前言一、问题分析二、报错原因三、解决思路检查类路径配置检查依赖库检查类文件调试类加载器问题四、常见

Java注解之超越Javadoc的元数据利器详解

《Java注解之超越Javadoc的元数据利器详解》本文将深入探讨Java注解的定义、类型、内置注解、自定义注解、保留策略、实际应用场景及最佳实践,无论是初学者还是资深开发者,都能通过本文了解如何利用... 目录什么是注解?注解的类型内置注编程解自定义注解注解的保留策略实际用例最佳实践总结在 Java 编程

Java 实用工具类Spring 的 AnnotationUtils详解

《Java实用工具类Spring的AnnotationUtils详解》Spring框架提供了一个强大的注解工具类org.springframework.core.annotation.Annot... 目录前言一、AnnotationUtils 的常用方法二、常见应用场景三、与 JDK 原生注解 API 的

Java controller接口出入参时间序列化转换操作方法(两种)

《Javacontroller接口出入参时间序列化转换操作方法(两种)》:本文主要介绍Javacontroller接口出入参时间序列化转换操作方法,本文给大家列举两种简单方法,感兴趣的朋友一起看... 目录方式一、使用注解方式二、统一配置场景:在controller编写的接口,在前后端交互过程中一般都会涉及

Java中的StringBuilder之如何高效构建字符串

《Java中的StringBuilder之如何高效构建字符串》本文将深入浅出地介绍StringBuilder的使用方法、性能优势以及相关字符串处理技术,结合代码示例帮助读者更好地理解和应用,希望对大家... 目录关键点什么是 StringBuilder?为什么需要 StringBuilder?如何使用 St

使用Java将各种数据写入Excel表格的操作示例

《使用Java将各种数据写入Excel表格的操作示例》在数据处理与管理领域,Excel凭借其强大的功能和广泛的应用,成为了数据存储与展示的重要工具,在Java开发过程中,常常需要将不同类型的数据,本文... 目录前言安装免费Java库1. 写入文本、或数值到 Excel单元格2. 写入数组到 Excel表格

Java并发编程之如何优雅关闭钩子Shutdown Hook

《Java并发编程之如何优雅关闭钩子ShutdownHook》这篇文章主要为大家详细介绍了Java如何实现优雅关闭钩子ShutdownHook,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起... 目录关闭钩子简介关闭钩子应用场景数据库连接实战演示使用关闭钩子的注意事项开源框架中的关闭钩子机制1.

Maven中引入 springboot 相关依赖的方式(最新推荐)

《Maven中引入springboot相关依赖的方式(最新推荐)》:本文主要介绍Maven中引入springboot相关依赖的方式(最新推荐),本文给大家介绍的非常详细,对大家的学习或工作具有... 目录Maven中引入 springboot 相关依赖的方式1. 不使用版本管理(不推荐)2、使用版本管理(推

Java 中的 @SneakyThrows 注解使用方法(简化异常处理的利与弊)

《Java中的@SneakyThrows注解使用方法(简化异常处理的利与弊)》为了简化异常处理,Lombok提供了一个强大的注解@SneakyThrows,本文将详细介绍@SneakyThro... 目录1. @SneakyThrows 简介 1.1 什么是 Lombok?2. @SneakyThrows

在 Spring Boot 中实现异常处理最佳实践

《在SpringBoot中实现异常处理最佳实践》本文介绍如何在SpringBoot中实现异常处理,涵盖核心概念、实现方法、与先前查询的集成、性能分析、常见问题和最佳实践,感兴趣的朋友一起看看吧... 目录一、Spring Boot 异常处理的背景与核心概念1.1 为什么需要异常处理?1.2 Spring B