[Java EE] 多线程(三):线程安全问题(上)

1. 线程安全

1.1 线程安全的概念

如果多线程环境下代码运行的结果不符合我们的预期,则我们说存在线程安全问题,即程序存在bug,反之,不存在线程安全问题.

1.2 线程不安全的原因

我们下面举出一个线程不安全的例子:我们想要在两个线程中对count进行++操作

public class Demo9 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});Thread thread1 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});thread.start();thread1.start();thread.join();thread1.join();System.out.println(count);}
}

运行结果如下:
在这里插入图片描述
但是这里我们预期的结果是100000,这里我们看到,实际结果和预期结果相差甚远,这便是产生了线程安全问题,使得程序出现了bug,我们要想解决上述的bug,我们必须先了解清楚bug产生的原因.

  1. 线程调度是随机的
    这是线程安全问题的罪魁祸首
    随机调度使⼀个程序在多线程环境下,执行顺序存在很多的变数.
    程序猿必须保证在任意执行顺序下,代码都能正常工作.
    某个线程在执行指令的过程中,当他执行任何一个指令的时候,都有可能被其他线程抢占走CPU.
  2. 修改共享数据
    多个线程同时修改同一个变量.上面的代码中,就都是针对count进行修改.
  3. 原子性
    在前面,我们有给大家提到过事务的原子性,大家还记得我们的助教迪卢克姥爷吗?
    在这里,多线程的原子性其实和事务的原子性大相径庭.我们在这里首先要理解什么是多线程中的原子性:

有请助教:达达利亚,钟离
达达利亚和钟离都到了一台ATM机前来取钱,现在每一台ATM机前都有一个门,一把锁,当达达利亚进去之后,门就会自动上锁,这样钟离便不会对达达利亚取钱的过程造成干扰,在达达利亚取完钱之前,钟离只可以在外面排队等待,在达达利亚取完钱之后,钟离才可以进入.也就是在tread线程对count进行修改的时候,tread1线程不可以对tread修改count的过程进行干扰,这便保证了原子性.反之如果钟离对达达利亚取钱的过程造成了干扰,这便不保证原子性.
在这里插入图片描述

一条Java语句不一定是原子的,也不一定是一条指令:
我们回到线程这里,那么如果拿上面这个存在线程安全问题的代码(不保证原子性的代码),那么他的底层原理是什么样子的呢:

  • 首先tread和tread1同时读到count=0
  • tread线程对count进行++之后放入内存之后,count变为1
  • tread1对线程进行++之后,对上一个count=1的值进行了覆盖,count还是1.
  • 这便会引起bug
    在这里插入图片描述
  1. 内存可见性
  2. 指令重排序
    后续介绍

1.3 解决线程安全问题

要想解决线程安全问题,我们必须要从原因来入手:

  • 从原因一入手:这是多线程已有的特性,无法干预.
  • 从原因二入手:这是一个切入点,**但是不普适,只针对特殊的场景可以做到,**比如String把变量设置为不可变对象,就是为了保证线程安全问题.在对上一个String进行修改的时候,其实在底层又new了一个新的String,修改的实际上不是同一个变量.
  • 从原因三入手:这是一个普适性比较高的切入点,我们想象,我们是否也可以有一把向ATM机那样的锁,来保证线程的原子性呢,答案是有.我们可以使用synchronized关键字来对线程进行上锁.通过上锁操作来把非原子的操作打包为一个原子的操作.保证tread线程对count计算的结果写入内存中在tread1线程读取内存中的count之后,使得它们呈现串行化执行.
  • 从原因四和五入手,后续介绍.

1.4 synchronized关键字—>监视器锁

为了解决上述线程安全问题,我们使用synchronized对上述代码的线程进行加锁:

public class Demo11 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Object o = new Object();//锁对象Thread thread = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o) {//线程上锁count++;}}});Thread thread1 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o){//拿到的都是o锁,产生锁互斥count++;}}});thread.start();thread1.start();thread.join();thread1.join();System.out.println(count);}
}
  • 首先,什么是锁:
    锁本质上是一个OS提供的功能,通过API给到了应用程序,JVM再对这样的API进行包装.这里我们就可以把锁简单地理解为一个不管类型,不管名字,不管是否存在泛型的任意变量,作用上有且只有一个,就是用来区分两个线程知否针对同一个对象加锁.
  • 如何对线程上锁:
    在一个线程中,在某一行使用synchronized ( )关键字,并在括号中传入锁对象,就证明从这一行的{开始,就开始对线程进行了上锁,直到}解锁.

当我们了解完synchronized的第一个特性之后,我们就知道上述上锁的过程是怎么回事了.

1.4.1 synchronized的使用实例

  1. 修饰代码块
public class SynchronizedDemo {private Object locker = new Object();public void method() {synchronized (locker) {//一系列操作}}
}
  1. 锁当前对象
public class SynchronizedDemo {public void method() {synchronized (this) {}}
}
  1. 直接修饰普通方法
public class SynchronizedDemo {public synchronized void methond() {}
}

一旦有线程调用该方法,就会上锁.

  1. 修饰静态方法
public class SynchronizedDemo {public synchronized static void method() {}
}

1.4.2 synchronized的特性

  1. 互斥性与锁竞争
    在tread线程对count进行++的时候,在count++的外围,我们使用synchronized关键字对count++进行了包裹,由于tread线程启动比tread1早,也就是在此时,线程tread已经拿到了o这把锁.此时由于tread1线程也在RUNNABLE状态,它也想拿到o这把锁.但是发现,o这把锁已经被tread线程占用了,只能阻塞等待,等待tread解锁.tread1进入BLOCKED状态.此时锁就产生了互斥性.
    解锁之后,由于系统调度线程的随机性,tread和tread1继续竞争o锁,便会产生锁竞争.
    在这里插入图片描述

我们举个例子来说明:
有请助教: 小乔,周瑜,兰陵王
由于兰陵王比周瑜先到一步,所以小乔先和兰陵王贴贴 了一段时间.在这里插入图片描述
兰陵王完事之后,兰陵王对小乔解锁,但是兰陵王又觉得自己还没有和小乔贴贴够,但是周瑜又向进去和自己的爱人贴贴,此时兰陵王和周瑜便产生了锁竞争,谁都向对小乔上锁.
在这里插入图片描述

如果两个线程对于两个不同的锁进行引用加锁,也就不会出现锁竞争问题:

public class Demo12 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Object o1 = new Object();Object o2 = new Object();Thread thread = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o1) {//o1对线程上锁count++;}}});Thread thread1 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o2){//拿到的是o2锁,不会产生锁互斥count++;//线程安全问题仍然存在}}});thread.start();thread1.start();thread.join();thread1.join();System.out.println(count);}
}

但是这样还是会产生线程安全问题.
运行结果:
在这里插入图片描述

讨论:join()和上锁的区别
join是在tread全部执行完成之后,再去执行tread1,而加锁是并发执行.
在join等待的时候是WAITING状态,而在上锁过程中是BLOCKED状态.在这里插入图片描述在这里插入图片描述

  1. 可重入与不可重入(死锁)
    我们思考这样几个场景:
  • 场景一:一个线程一把锁
public class Demo13 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Object o = new Object();Thread thread = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o) {//对线程上锁synchronized (o){//又上了一次锁count++;}}}});thread.start();thread.join();System.out.println(count);}
}

上面的代码tread两次利用o上锁,我们来思考,在第二次上锁的时候,会不会因为锁的互斥性,而使得tread线程产生阻塞,那就自己把自己锁死了,产生便了死锁.

举例说明:
有请助教:钟离
假如钟离在上厕所…在这里插入图片描述

如果产生上述情况,我们称该锁为不可重入锁.如c++,python中自带的锁,都是不可重入锁,一旦像上面那样写,就锁死了.
但是Java中的锁是可重入锁,对一个线程使用相同的锁进行多次加锁之后,不会出现锁死的情况.不会产生锁冲突.可见Java的创始者为了不让我们Java程序员写出bug,真的是操碎了心!!!
在这里插入图片描述
可重入锁的原理:
在可重⼊锁的内部,包含了"线程持有者"和"计数器"两个信息.
• 如果某个线程加锁的时候,先判断这个线程是否被加锁,如果没有,则加锁,如果发现锁已经被⼈占⽤,但是恰好占⽤的正是⾃⼰,那么仍然可以继续获取到锁,并让计数器⾃增.
• 解锁的时候计数器递减为0的时候,才真正释放锁.(才能被别的线程获取到)

举例说明:
有请助教:小乔,周瑜,兰陵王

在这里插入图片描述
在这里插入图片描述

那么什么时候Java会产生死锁呢?

  • 场景二:两个线程两把锁
public class Demo14 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Object o = new Object();Object o1 = new Object();Thread thread = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o) {//1.拿到o锁synchronized (o1){//3.与tread1的o1锁互斥count++;}}}});Thread thread1 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o1){//2.拿到o1锁synchronized (o){//4.与tread的o锁互斥count++;}}}//3,4相互等待,最终卡死});thread.start();thread1.start();thread.join();thread1.join();System.out.println(count);}
}

运行结果:毛都没有!
在这里插入图片描述
这就说明,这里的的进程卡死,产生了死锁.
什么原理呢?由于tread一首先启动,tread拿到o锁,并上锁,此时tread1启动,拿到o1锁,当tread想要拿到o1锁的时候,发现o1锁被占用,阻塞等待,当tread1想要拿到o锁的时候,发现o锁被占用,阻塞等待,这时候tread1和tread相互循环相互等待,就产生了死锁.
这就像两个相互暗恋的人一样,都彼此暗恋着对方,但是都不敢鼓起勇气去表白,这样就会彼此错过.
在这里插入图片描述

  • 那么我们如何规避死锁呢?(重点面试题)
    首先我们要知道参数死锁的4个必要条件:
  1. 锁具有互斥性
  2. 锁不可剥夺
    上述是锁的两个基本的特性,我们无法干预
  3. 请求锁和保持锁
    一个线程拿到一把锁之后,不释放这个锁,就尝试获取其他锁.
  4. 循环等待:
    多个线程获取多个锁过程中,A等待B,B等待A.
    上述两个条件,我们都可以通过干预代码结构来解除死锁.
    我们需要约定好加锁顺序,让所有的线程按照一定的顺序加锁.
    我们尝试使用上面的方法对上面的场景二的死锁进行解除:调换lock1和lock2的位置,让tread执行完所有的逻辑之后释放锁之后,再轮到tread1执行.
public class Demo14 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Object o = new Object();Object o1 = new Object();Thread thread = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o) {synchronized (o1){count++;}}}});Thread thread1 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (o){synchronized (o1){count++;}}}});thread.start();thread1.start();thread.join();thread1.join();System.out.println(count);}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/1418.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

山东大学操作系统实验一(Linux虚拟机实现)

目录 实验题目 实验要求 示例程序 主程序 头文件 重点代码解析 一、main函数的参数 参数介绍 参数输入方式 本块代码 二、信号处理 本块代码 原理介绍 实现效果 三、kill函数 功能介绍 使用方式 本块代码 四、头文件处理 本块代码 代码作用 实验程序 …

微调Llama3实践并基于Llama3构建心理咨询EmoLLM

Llama3 Xtuner微调Llama3 EmoLLM 心理咨询师

【嵌入式DIY实例】-称重计

DIY称重计 文章目录 DIY称重计1、硬件准备1.1 HX711 称重传感器模块2、硬件接线原理图3、代码实现在本文中,我们将使用数字体重秤 HX711 称重传感器模块来实现一个简易的称重计。 HX711 模块非常适合测量重量、力或任何其他可以以克为单位的东西。 该模块易于使用,可以连接到…

SpringBoot-餐饮业供应商管理系统-94116

SpringBoot餐饮业供应商管理系统 摘 要 随着餐饮业竞争的加剧&#xff0c;不仅需要有吸引力的菜肴&#xff0c;还需要先进的管理手段&#xff0c;才能在餐饮业站稳脚跟。通过完善的餐饮业供应商管理系统&#xff0c;不仅可以帮助餐饮企业在物流配送、商品管理等方面有所改进&a…

代码随想录总结|60天代码随想录训练结束(图论没开)

今年2月19日晚上我辗转反侧&#xff0c;看着目标院校复试群发呆。大学3年&#xff0c;前两年生病养病&#xff0c;后半年家里出了状况&#xff0c;玉玉了半学期。算是一事无成了&#xff0c;寒假尝试着刷LeetCode&#xff0c;就从B站上找教程&#xff0c;就找到了卡哥。看了一下…

Python根据公募基金在一定时期内持有的股票数据进行社会网络分析

【背景】根据提供的公募基金在一定时期内持有的股票数据&#xff0c;构建一个社会网络分析框架&#xff0c;度量每个基金在每年的度中心度、介数中心度和特征向量中心度&#xff0c;并对相关数据做出简要说明。 【代码】 import networkx as nx import pandas as pd import n…

IDEA2024配置RunDashBoard(Services)面板

IDEA2024配置RunDashBoard(Services)面板 新版本的IDEA没有RunDashBoard&#xff0c;取而代之的是Services面板&#xff0c;不需要配置workspace.xml文件; 本文教你简单的方法就能一个SpringBoot的Main运行多次&#xff0c;方便调试。 1、配置启动类 导航栏&#xff0c;Edit…

使用立创EDA打开JSON格式的PCB及原理图

一、将PCB和原理图放同一文件夹 并打包成.zip文件 二、打开嘉立创EDA并导入.zip文件 文件 -> 导入 -> 嘉立创EDA标准版/专业版 三、选择.zip文件并选择 “导入文件并提取库” 四、自定义工程路径 完成导入并转换为.eprj文件 五、视频教学 bilibili_使用立创EDA打开JSO…

Pytorch入门实战: 06-VGG-16算法-Pytorch实现人脸识别

第P6周&#xff1a;VGG-16算法-Pytorch实现人脸识别 &#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客 &#x1f356; 原作者&#xff1a;K同学啊 &#x1f3e1; 我的环境&#xff1a; 语言环境&#xff1a;Python3.8 编译器&#xff1a;Jupyter La…

Rest接口/Nginx日志记录和采集

文章目录 一、Rest接口日志二、Nginx日志三、采集日志四、夜莺查看Nginx日志五、夜莺查看Rest接口日志 一、Rest接口日志 记录日志字典定义 接口URL接口名称,类别,入参全记录,出参全记录,入参字段1:中文名1/入参字段2:中文名2,出参字段1:中文名1/test/api/login账户登录,登录…

Redis 逻辑过期策略设计思路

引言&#xff1a; 当我们平常使用Redis缓存的时候&#xff0c;会出现一种场景&#xff0c; redis的key到过期时间了&#xff0c;总是需要到数据库里面去查一遍数据再set回redis&#xff0c;这个时候如果数据库响应比较慢&#xff0c;那么就会造成用户等待&#xff0c;如果刚好…

09 JavaScript学习:对象

对象的概念 在计算机科学中&#xff0c;对象是一种数据结构&#xff0c;用于存储数据和方法&#xff08;也称为函数&#xff09;。对象可以包含属性&#xff08;也称为成员变量&#xff09;和方法&#xff08;也称为成员函数&#xff09;&#xff0c;通过这些属性和方法可以描述…

【微信小程序】解决分页this.setData数据量太大的限制问题

1、原始方法&#xff0c;每请求一页都拿到之前的数据concat一下后整体再setData loadData() {let that thislet data {}data.page this.data.pagedata.size this.data.sizefindAll(data).then(res > {if (res.data.code 1) {this.setData({dataList: this.data.dataLi…

android开发 多进程的基本了解

目录 如何开启多进程?理解多进程模式的运行机制 如何开启多进程? 给四大组件在androidMenifest中指定android:precess <activityandroid:name".ThreeActivity"android:exported"false"android:process"com.my.process.three.remote" />…

如何查询下载自然资源相关的法律法规

自然资源部门户网站- 政策法规库 (https://f.mnr.gov.cn/) 以查询下载“节约用水条例”为例&#xff1a;输入标题&#xff0c;点击检索&#xff0c;出现对应的检索结果&#xff1a; 打开详细&#xff0c;可以看到节约用水条例的详细内容&#xff1a; 点击文后的打印或者下载&a…

如何使用PHPStudy+Cloudreve搭建个人云盘并实现无公网IP远程访问——“cpolar内网穿透”

文章目录 1、前言2、本地网站搭建2.1 环境使用2.2 支持组件选择2.3 网页安装2.4 测试和使用2.5 问题解决 3、本地网页发布3.1 cpolar云端设置3.2 cpolar本地设置 4、公网访问测试5、结语 1、前言 自云存储概念兴起已经有段时间了&#xff0c;各互联网大厂也纷纷加入战局&#…

day48_servlet

今日内容 周一 0 复习上周 1 本周计划 2 MVC和三层架构 3 Login案例 4 请求转发 5 重定向 0 复习昨日 1 jdbc五大步骤 注册驱动(反射)获得连接获得执行sql对象执行SQL关流 2 什么是SQL注入 通过SQL关键词,在执行SQL时出现不正常的情况 3 PreparedStatement怎么使用,有什么特点 …

刷题之Leetcode19题(超级详细)

19.删除链表的倒数第N个节点 力扣题目链接(opens new window)https://leetcode.cn/problems/remove-nth-node-from-end-of-list/ 给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 进阶&#xff1a;你能尝试使用一趟扫描实现吗&#x…

react09 hooks(useState)

react-09 hooks&#xff08;useState&#xff09; hooks组件&#xff08;函数组件动态化&#xff09; 其本质就是函数组件&#xff0c;引用一些hooks方法&#xff0c;用来在函数组件中进行例如状态管理&#xff0c;模拟类组件的生命周期等&#xff0c;只能运用到函数组件中 ho…

代码随想录算法训练营第三十二天|122.买卖股票的最佳时机II,55. 跳跃游戏,45.跳跃游戏II

目录 122.买卖股票的最佳时机II思路代码 55. 跳跃游戏思路代码 45.跳跃游戏II思路代码 122.买卖股票的最佳时机II 题目链接&#xff1a;122.买卖股票的最佳时机II 文档讲解&#xff1a;代码随想录 视频讲解&#xff1a;贪心算法也能解决股票问题&#xff01;LeetCode&#xff1…