JavaEE-多线程初阶(3)
目录
1.线程的状态
1.1 NEW、RUNNABLE、TERMINATED
1.2 TIMED_WAITING
1.3 WAITING
1.4 BLOCKED
2.多线程带来的风险-线程安全(重点)
2.1 观察线程不安全的现象
2.2 分析产生该现象的原因
2.3 产生线程安全问题的原因
2.3.1 抢占式执行(根本)
2.3.2 多个线程同时修改同一个变量
2.3.3 修改操作,不是原子的
2.3.4 内容可见性问题
2.3.5 指令从排序
2.4 如何解决线程安全问题
2.4.1 抢占式执行
2.4.2 多个线程同时修改同一个变量
2.4.3 修改操作,不是原子的
2.4.3.1 锁的概念
2.4.3.2加锁:synchronized
2.4.3.3 synchronized的变种写法
2.4.3.4 可重入
2.4.3.5 监视器锁
1.线程的状态
线程的所有状态如下:
• NEW: 安排了⼯作, 还未开始⾏动• RUNNABLE: 可⼯作的. ⼜可以分成正在⼯作中和即将开始⼯作.• BLOCKED: 这⼏个都表⽰排队等着其他事情• WAITING: 这⼏个都表⽰排队等着其他事情• TIMED_WAITING: 这⼏个都表⽰排队等着其他事情• TERMINATED: ⼯作完成了.
1.1 NEW、RUNNABLE、TERMINATED
对于这三个状态的解释:
NEW:new了Thread对象,还没start
RUNNABLE :就绪状态,可以分成以下两种情况(正在工作或者即将开始工作):
(1)线程正在cpu上执行(2)线程随时可以去cpu上执行TERMINATED:内核中的线程已经结束了,但是Thread对象还在
public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{System.out.println("线程t1开始执行...");try {Thread.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t1线程结束...");});System.out.println(t1.getState());t1.start();Thread.sleep(0,500);System.out.println(t1.getState());t1.join();System.out.println(t1.getState());}
执行结果:
1.2 TIMED_WAITING
• 指定时间的阻塞
• 线程阻塞(不参与 cpu 调度,不继续执行了)
• 阻塞的时间是有上限的
Thread.sleep(时间);会进入到TIMED_WAITING状态
另外,join(时间)也会进入到TIMED_WAITING状态:
1.3 WAITING
死等,没有超时时间的阻塞等待
1.4 BLOCKED
也是一种阻塞,比较特殊,是由于锁导致的阻塞。
2.多线程带来的风险-线程安全(重点)
2.1 观察线程不安全的现象
案例:
创建一个静态变量count=0;
在main方法里创建两个线程t1和t2
两个线程内分别循环五万次count++操作
最后打印出count的值:
public class Demo10 {public static int count=0;public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});Thread t2=new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}
按照道理来说,count进行了总共十万次count++操作,打印出来的值应该是十万,实际上却并非如此,执行代码:
这样的代码很明显是有bug的,实际执行效果与预期效果不符合就叫做bug。
这样的问题,是多线程并发执行引发的问题。
如果调换代码的执行程序,把t1线程和t2线程的并行改成串行(一个执行完了再执行另一个)
结果又变成正确的了:
执行结果:
很明显,当前的bug是由于多线程的并发执行代码引起的bug
这样的bug,就称为“线程安全问题”,或者叫做“线程不安全”
反之,如果一个代码在多线程并发执行的环境下也不会出现类似上述的bug
这样的代码就称为“线程安全”
2.2 分析产生该现象的原因
站在 CPU 的角度来看count++,这个操作看起来是一行代码,实际上对应到三个CPU指令:
1.load,把内存中的值(count变量)读取到寄存器中
2.add,把指定的寄存器中的值进行+1操作(结果还是在这个寄存器中)
3.save,把寄存器中的值,写回到内存中
CPU执行这三条指令的过程中,随时有可能触发线程的调度切换(如下所示):
指令1 指令2 指令3 线程切走
指令1 指令2 线程切走 指令3
指令1 线程切走 指令2 指令3
指令1 线程切走 指令2 线程切走 指令3
但是由于操作系统的调度是随机的,不可预期的
执行任何一个指令的过程中都有可能触发上述的“线程切换”的操作
也就是说,随机调度(或者叫抢占式执行)就是线程安全问题的罪魁祸首。
t1线程和t2线程的执行过程可能会出现如下情况(只举了个别例子,可能还有别的情况):
对于执行结果:打 √ 的为正确结果,× 为错误结果。
对于其中某种正确情况的具体执行过程如下:
对于其中某种错误情况的具体执行过程如下:
可以看到:
t1线程和t2线程分别进行了一次count++操作,理论上count的值应为2,实际上由于随机调度,t1的寄存器和t2的寄存器都读取了内存的初始值0,最终执行完两次count++后count的值为1
由于线程执行的过程中有可能(大概率)会发生上述的错误情况,因此代码的执行结果总是<=100000
如果降低单个线程的循环次数,会发现能运行出正确的结果:
运行结果:
出现这种现象的原因:
事实上,线程安全问题依旧存在,只不过概率变低了。
单个线程执行的count++次数会影响到这个线程的运行时间,即运行50次比运行50000次要快得多
因为运行的太快了,很可能在t2.start()执行之前,t1线程就执行完了
等到后续t2线程的执行,就变成了串行执行
2.3 产生线程安全问题的原因
2.3.1 抢占式执行(根本)
抢占式执行:操作系统对于线程的调度是随机的
抢占式执行策略
最初诞生于多任务操作系统的时候,是非常重大的发明
后世的操作系统,都是一脉相承
2.3.2 多个线程同时修改同一个变量
如果是一个线程,修改一个变量---没问题
如果是多个线程,不是同时修改同一个变量---没问题
如果是多个线程,修改不同变量---没问题(不会出现中间结果相互覆盖的情况)
如果是多个线程,读取同一个变量---没问题(该变量不可修改)
2.3.3 修改操作,不是原子的
原子:
如果一个操作只是对应到一个cpu指令,那么就可以认为它是原子的
cpu就不会出现“一条指令执行到一半”这样的情况
反之,如果一个操作对应到多个cpu指令,那么它就不是原子的,例如:
++
--
+=
-=
........
2.3.4 内容可见性问题
后续再讨论
2.3.5 指令从排序
后续再讨论
2.4 如何解决线程安全问题
2.4.1 抢占式执行
抢占式执行是操作系统的底层设定,程序员无法左右。
2.4.2 多个线程同时修改同一个变量
和代码的结构直接相关
可以调整代码结构,规避一些线程不安全的代码
但是这样的方案不够通用
Java中有个东西:
String 就是采用了“不可变”特性,确保线程安全
2.4.3 修改操作,不是原子的
解决方案:
Java中解决线程安全问题,最主要的方案就是:加锁
通过加锁操作,让不是原子的操作,打包成一个原子的操作
2.4.3.1 锁的概念
计算机中的锁,和生活中的锁,是同样的概念:互斥/排他
把锁“锁上”称为“加锁”
把锁“解开”称为“解锁”
一旦把锁加上了,其他人要想加锁,就得阻塞等待
对于上述例子的count++操作,它并非是原子的,此时就可以使用锁,把刚才不是原子的count++
包裹起来,在count++之前,先加锁,然后进行count++,计算完毕之后,再解锁
(此时在执行三步走的过程中,其他线程就没法“插队”了)
加锁操作,不是把线程锁死到cpu上,禁止这个线程被调度走
而是禁止其他线程重新加这个锁,避免其他线程的操作在当前线程执行的过程中“插队”
2.4.3.2加锁:synchronized
加锁 / 解锁 本身是操作系统提供的api
很多编程语言都对这样的api进行了封装
大多数的封装风格,都是采取两个函数:
加锁 lock();
//执行一些要保护起来的逻辑
解锁 unlock();
Java中使用 synchronized 这样的关键字,搭配代码块来实现类似的效果:
synchronized(){ //进入代码块,相当于加锁
//执行一些要保护起来的逻辑
}出了代码块,相当于解锁
使用synchronized对上述案例进行加锁:
可以看到括号内需要填入参数,应该填入什么呢?
填写的是,用来加锁的对象。要 加锁 / 解锁,前提是得先有一个锁
在Java中任何一个对象都可以用作“锁”
这个对象的类型是什么不重要
重要的是,是否有多个线程针对这同一个对象加锁(竞争同一把锁)
再次执行代码,线程安全问题得到了解决:
【注意】
两个线程,针对同一个对象加锁,才会产生互斥效果
(一个线程加上了锁,另一个线程就得阻塞等待,等到第一个线程释放锁,才有机会)
如果是不同的锁对象,此时不会有互斥效果,线程安全问题并没有得到解决:
代码执行结果,依旧存在线程安全问题:
2.4.3.3 synchronized的变种写法
在方法内加锁:
这种写法跟之前的写法产生的效果是一样的,执行代码:
修饰方法的写法还能继续变形:使用synchronized修饰方法,就相当于是针对this进行加锁
这种写法跟上面的效果也是一样的。
StringBuffer、Vector这些类里面有些方法就是带有synchronized的:
因此,StringBuffer是线程安全的,而StringBuilder线程不安全。
而方法之中,还有一个特殊情况,static修饰的方法不存在this
此时,synchronized修饰static方法相当于针对类对象加锁。
2.4.3.4 可重入
当我们使用锁写代码,一旦方法调用的层次比较深,搞不好就会出现这种情况(或者类似的):
分析上面这段代码:
1.第一次进行加锁(第一层),能够成功(锁没有人使用)
2.第二次进行加锁(第二层),此时的locker已经是被占用的状态,第二次加锁就会触发阻塞等待
要想解除阻塞,就必须往下执行
要想往下执行,就需要等到第一次的锁被释放
这样的问题,就被称为“死锁”(dead lock)
为了解决上述问题,Java的synchronized就引入了可重入的概念
当某个线程针对一个锁,加锁成功之后
后续该线程再次针对这个锁进行加锁
不会触发阻塞,而是直接往下走
因为当前这把锁就是被这个线程持有
但是如果是其他线程尝试加锁,就会正常阻塞
也就是说,当我们执行上述代码时,并不会触发死锁,而是正常执行:
可重入锁的实现原理,关键在于让锁对象内部保存,当前是哪个线程持有这把锁
后续有线程针对这个锁加锁的时候,对比一下,持有锁的线程是否和当前要加锁的线程是同一个
当有多层加锁时,最外层的加锁和解锁才是有效的,内部锁直接放行
站在JVM的角度,看到多个 } 需要执行,JVM如何知道哪个 } 是真正需要解锁的那个:
先引入一个变量,计数器(0)
每次触发 { 的时候,把计数器++
每次触发 } 的时候,把计数器--
当计数器 --到0的时候,就是真正需要解锁的时候
2.4.3.5 监视器锁
监视器锁 minitor lock
JVM中采用的一个术语
使用锁的过程中抛出的一些异常,可能会看到 监视器锁 这样的报错信息
完
如果哪里有疑问的话欢迎来评论区指出和讨论,如果觉得文章有价值的话就请给我点个关注还有免费的收藏和赞吧,谢谢大家
相关文章:

JavaEE-多线程初阶(3)
目录 1.线程的状态 1.1 NEW、RUNNABLE、TERMINATED 1.2 TIMED_WAITING 1.3 WAITING 1.4 BLOCKED 2.多线程带来的风险-线程安全(重点) 2.1 观察线程不安全的现象 2.2 分析产生该现象的原因 2.3 产生线程安全问题的原因 2.3.1 抢占式执行&#x…...
从入门到精通:如何在Vue项目中有效运用el-image-viewer
Element UI之el-image-viewer组件详解 引言 在现代 Web 应用中,高质量的用户体验是不可或缺的一环。Element UI 作为一款基于Vue.js 2.0 的桌面端组件库,以其丰富的组件集、良好的文档和支持赢得了广大开发者的好评。本文将深入探讨el-image-viewer组件,这是一个用于在网页…...

uniapp组件实现省市区三级联动选择
1.导入插件 先将uni-data-picker组件导入我们的HBuilder项目中,在DCloud插件市场搜索uni-data-picker 点击下载插件并导入到我们的项目中 2.组件调用 curLocation :获取到的当前位置(省市区) <uni-data-picker v-slot:defa…...

【C++】异常处理机制(对运行时错误的处理)
🌈 个人主页:谁在夜里看海. 🔥 个人专栏:《C系列》《Linux系列》 ⛰️ 天高地阔,欲往观之。 目录 引言 1.编译器可以处理的错误 2.编译器不能处理的错误 3.传统的错误处理机制 assert终止程序 返回错误码 一、…...
C++ boost steady_timer使用介绍
文章目录 1. 引入必要的头文件2. 基本用法2.1 同步定时器解释:2.2 异步定时器解释:3. 异步定时器与回调函数4. 设置定时器的超时时间4.1 使用秒、毫秒、微秒4.2 修改定时器的到期时间5. 多次使用定时器6. 循环执行任务7. 错误处理总结:C++ Boost 库提供了 boost::asio::stea…...
JVM 由多个模块组成,每个模块负责特定的功能
Java虚拟机(JVM, Java Virtual Machine)是一个抽象的计算机,它提供了一个运行环境,使得Java字节码可以在不同的平台上执行。JVM 由多个模块组成,每个模块负责特定的功能。以下是 JVM 的主要模块及其功能: …...
ORACLE批量插入更新如何拆分大事务?
拆分大事务 一、批量插入更新二、拆分事务之前文章MYSQL批量插入更新如何拆分大事务?说明了Mysql如何拆分,本篇文章探讨Oracle或OceanBase批量插入更新拆分大事务的问题 一、批量插入更新 oracle批量插入更新可使用merge语法eg: merge test ausing test_tmp bon (a.id = b.id…...

kafka+zookeeper的搭建
kafka从2.8版本开始,就可以不用配置zookeeper了,但是也可以继续配置。我目前使用的kafka版本是kafka_2.12-3.0.0.tgz,其中前面的2.12表示是使用该版本的scala语言进行编写的,而后面的3.00才是kafka当前的版本。 通过百度网盘分享…...

Spark中的宽窄依赖
一、什么是依赖关系 这里通过一张图来解释: result_rdd是由tuple_rdd使用reduceByKey算子得到的, 而tuple_rdd是由word_rdd使用map算子得到的,word_rdd又是由input_rdd使用flatMap算子得到的。它们之间的关系就称为依赖关系! 二…...

安装和运行开发微信小程序
下载HBuilder uniapp官网 uni-app官网 微信开发者工具 安装 微信小程序 微信小程序 官网 微信小程序 配置 运行 注意:运行前需要开启服务端口 如果运行看不到效果,设置下基础库选别的版本 配置...
地图框架之mapbox——(五)
今天主要学习mapbox中如何使用画笔! 一、导入画笔依赖 <script src"https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-draw/v1.2.2/mapbox-gl-draw.js"></script> <link rel"stylesheet" href"https://api.mapbox…...
Hive 的数据类型
基本类型 整型 TINYINT: 1字节整数,范围从 -128 到 127。SMALLINT: 2字节整数,范围从 -32,768 到 32,767。INT: 4字节整数,范围从 -2,147,483,648 到 2,147,483,647。BIGINT: 8字节整数,范围从 -9,223,372,036,854,775,808 到 9…...

2024下半年软考考后估分,快来预约!
2024下半年软考这周末就要开考了!考后大家最关心的,莫过于考试成绩。届时会为家更新回忆版真题及答案,现在就可以开始预约啦~ 因为是回忆版,老师做题也需要时间,答案会慢慢更新,大家耐心等待片刻ÿ…...

第8章 利用CSS制作导航菜单作业
1.利用CSS技术,结合链接和列表,设计并实现“山水之间”页面。 浏览效果如下: HTML代码如下: <!DOCTYPE html> <html><head><meta charset"utf-8" /><title>山水之间</title><…...

基于Spring Boot的船舶监造系统的设计与实现,LW+源码+讲解
摘要 近年来,信息化管理行业的不断兴起,使得人们的日常生活越来越离不开计算机和互联网技术。首先,根据收集到的用户需求分析,对设计系统有一个初步的认识与了解,确定船舶监造系统的总体功能模块。然后,详…...
linux强制修改mysql的root账号密码
在Linux环境下,如果您忘记了MySQL的root密码,可以通过以下步骤来强制修改root密码: 在执行这些步骤之前,请确保您有足够的权限来执行这些命令。 停止MySQL服务: systemctl stop mysql 启动MySQL的安全模式,…...

CentOS系统查看CPU、内存、操作系统等信息
Linux系统提供了一系列命令可以用来查看系统硬件信息,如CPU的物理个数、核数、逻辑CPU数量、内存信息和操作系统版本。 查看物理CPU、核数和逻辑CPU 在多核、多线程的系统中,了解物理CPU个数、每个物理CPU的核数和逻辑CPU个数至关重要。超线程技术进一步…...

针对解决前后端BUG的个人笔记
1-IDEA Q:Required Java version 17 is not supported by SDK 1.8. The maximum supported Java version is 8. A: 我们只知道IDEA页面创建Spring项目,其实是访问spring initializr去创建项目。故我们可以通过阿里云国服去间接创建Spring项目。将https…...

5G时代已来:我们该如何迎接超高速网络?
内容概要 随着5G技术的普及,我们的生活似乎变得更加“科幻”了。想象一下,未来的智能家居将不仅仅是能够听你说“开灯”;它们可能会主动询问你今天心情如何,甚至会推荐你一杯“维他命C芒果榨汁”,帮助你抵御夏天的炎热…...
企业级-实现Redis封装层
作者:fyupeng 技术专栏:☞ https://github.com/fyupeng 项目地址:☞ https://github.com/fyupeng/distributed-blog-system-api 留给读者 封装 Redis 客户端Dao层、分布式锁等。 一、介绍 二、代码 DataInitialLoadRunner.java /*** Clas…...
day52 ResNet18 CBAM
在深度学习的旅程中,我们不断探索如何提升模型的性能。今天,我将分享我在 ResNet18 模型中插入 CBAM(Convolutional Block Attention Module)模块,并采用分阶段微调策略的实践过程。通过这个过程,我不仅提升…...

Mysql中select查询语句的执行过程
目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析(Parser) 2.4、执行sql 1. 预处理(Preprocessor) 2. 查询优化器(Optimizer) 3. 执行器…...

【p2p、分布式,区块链笔记 MESH】Bluetooth蓝牙通信 BLE Mesh协议的拓扑结构 定向转发机制
目录 节点的功能承载层(GATT/Adv)局限性: 拓扑关系定向转发机制定向转发意义 CG 节点的功能 节点的功能由节点支持的特性和功能决定。所有节点都能够发送和接收网格消息。节点还可以选择支持一个或多个附加功能,如 Configuration …...
Oracle11g安装包
Oracle 11g安装包 适用于windows系统,64位 下载路径 oracle 11g 安装包...
用鸿蒙HarmonyOS5实现中国象棋小游戏的过程
下面是一个基于鸿蒙OS (HarmonyOS) 的中国象棋小游戏的实现代码。这个实现使用Java语言和鸿蒙的Ability框架。 1. 项目结构 /src/main/java/com/example/chinesechess/├── MainAbilitySlice.java // 主界面逻辑├── ChessView.java // 游戏视图和逻辑├──…...
DiscuzX3.5发帖json api
参考文章:PHP实现独立Discuz站外发帖(直连操作数据库)_discuz 发帖api-CSDN博客 简单改造了一下,适配我自己的需求 有一个站点存在多个采集站,我想通过主站拿标题,采集站拿内容 使用到的sql如下 CREATE TABLE pre_forum_post_…...

热门Chrome扩展程序存在明文传输风险,用户隐私安全受威胁
赛门铁克威胁猎手团队最新报告披露,数款拥有数百万活跃用户的Chrome扩展程序正在通过未加密的HTTP连接静默泄露用户敏感数据,严重威胁用户隐私安全。 知名扩展程序存在明文传输风险 尽管宣称提供安全浏览、数据分析或便捷界面等功能,但SEMR…...

相关类相关的可视化图像总结
目录 一、散点图 二、气泡图 三、相关图 四、热力图 五、二维密度图 六、多模态二维密度图 七、雷达图 八、桑基图 九、总结 一、散点图 特点 通过点的位置展示两个连续变量之间的关系,可直观判断线性相关、非线性相关或无相关关系,点的分布密…...

leetcode73-矩阵置零
leetcode 73 思路 记录 0 元素的位置:遍历整个矩阵,找出所有值为 0 的元素,并将它们的坐标记录在数组zeroPosition中置零操作:遍历记录的所有 0 元素位置,将每个位置对应的行和列的所有元素置为 0 具体步骤 初始化…...

理想汽车5月交付40856辆,同比增长16.7%
6月1日,理想汽车官方宣布,5月交付新车40856辆,同比增长16.7%。截至2025年5月31日,理想汽车历史累计交付量为1301531辆。 官方表示,理想L系列智能焕新版在5月正式发布,全系产品力有显著的提升,每…...