《疯狂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

相关文章

Spring LDAP目录服务的使用示例

《SpringLDAP目录服务的使用示例》本文主要介绍了SpringLDAP目录服务的使用示例... 目录引言一、Spring LDAP基础二、LdapTemplate详解三、LDAP对象映射四、基本LDAP操作4.1 查询操作4.2 添加操作4.3 修改操作4.4 删除操作五、认证与授权六、高级特性与最佳

Spring Shell 命令行实现交互式Shell应用开发

《SpringShell命令行实现交互式Shell应用开发》本文主要介绍了SpringShell命令行实现交互式Shell应用开发,能够帮助开发者快速构建功能丰富的命令行应用程序,具有一定的参考价... 目录引言一、Spring Shell概述二、创建命令类三、命令参数处理四、命令分组与帮助系统五、自定义S

SpringSecurity JWT基于令牌的无状态认证实现

《SpringSecurityJWT基于令牌的无状态认证实现》SpringSecurity中实现基于JWT的无状态认证是一种常见的做法,本文就来介绍一下SpringSecurityJWT基于令牌的无... 目录引言一、JWT基本原理与结构二、Spring Security JWT依赖配置三、JWT令牌生成与

Java中Date、LocalDate、LocalDateTime、LocalTime、时间戳之间的相互转换代码

《Java中Date、LocalDate、LocalDateTime、LocalTime、时间戳之间的相互转换代码》:本文主要介绍Java中日期时间转换的多种方法,包括将Date转换为LocalD... 目录一、Date转LocalDateTime二、Date转LocalDate三、LocalDateTim

如何配置Spring Boot中的Jackson序列化

《如何配置SpringBoot中的Jackson序列化》在开发基于SpringBoot的应用程序时,Jackson是默认的JSON序列化和反序列化工具,本文将详细介绍如何在SpringBoot中配置... 目录配置Spring Boot中的Jackson序列化1. 为什么需要自定义Jackson配置?2.

Java中使用Hutool进行AES加密解密的方法举例

《Java中使用Hutool进行AES加密解密的方法举例》AES是一种对称加密,所谓对称加密就是加密与解密使用的秘钥是一个,下面:本文主要介绍Java中使用Hutool进行AES加密解密的相关资料... 目录前言一、Hutool简介与引入1.1 Hutool简介1.2 引入Hutool二、AES加密解密基础

Spring Boot项目部署命令java -jar的各种参数及作用详解

《SpringBoot项目部署命令java-jar的各种参数及作用详解》:本文主要介绍SpringBoot项目部署命令java-jar的各种参数及作用的相关资料,包括设置内存大小、垃圾回收... 目录前言一、基础命令结构二、常见的 Java 命令参数1. 设置内存大小2. 配置垃圾回收器3. 配置线程栈大小

SpringBoot实现微信小程序支付功能

《SpringBoot实现微信小程序支付功能》小程序支付功能已成为众多应用的核心需求之一,本文主要介绍了SpringBoot实现微信小程序支付功能,文中通过示例代码介绍的非常详细,对大家的学习或者工作... 目录一、引言二、准备工作(一)微信支付商户平台配置(二)Spring Boot项目搭建(三)配置文件

解决SpringBoot启动报错:Failed to load property source from location 'classpath:/application.yml'

《解决SpringBoot启动报错:Failedtoloadpropertysourcefromlocationclasspath:/application.yml问题》这篇文章主要介绍... 目录在启动SpringBoot项目时报如下错误原因可能是1.yml中语法错误2.yml文件格式是GBK总结在启动S

Spring中配置ContextLoaderListener方式

《Spring中配置ContextLoaderListener方式》:本文主要介绍Spring中配置ContextLoaderListener方式,具有很好的参考价值,希望对大家有所帮助,如有错误... 目录Spring中配置ContextLoaderLishttp://www.chinasem.cntene