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…...
KubeSphere 容器平台高可用:环境搭建与可视化操作指南
Linux_k8s篇 欢迎来到Linux的世界,看笔记好好学多敲多打,每个人都是大神! 题目:KubeSphere 容器平台高可用:环境搭建与可视化操作指南 版本号: 1.0,0 作者: 老王要学习 日期: 2025.06.05 适用环境: Ubuntu22 文档说…...
接口测试中缓存处理策略
在接口测试中,缓存处理策略是一个关键环节,直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性,避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明: 一、缓存处理的核…...
Xshell远程连接Kali(默认 | 私钥)Note版
前言:xshell远程连接,私钥连接和常规默认连接 任务一 开启ssh服务 service ssh status //查看ssh服务状态 service ssh start //开启ssh服务 update-rc.d ssh enable //开启自启动ssh服务 任务二 修改配置文件 vi /etc/ssh/ssh_config //第一…...
AtCoder 第409场初级竞赛 A~E题解
A Conflict 【题目链接】 原题链接:A - Conflict 【考点】 枚举 【题目大意】 找到是否有两人都想要的物品。 【解析】 遍历两端字符串,只有在同时为 o 时输出 Yes 并结束程序,否则输出 No。 【难度】 GESP三级 【代码参考】 #i…...
CMake 从 GitHub 下载第三方库并使用
有时我们希望直接使用 GitHub 上的开源库,而不想手动下载、编译和安装。 可以利用 CMake 提供的 FetchContent 模块来实现自动下载、构建和链接第三方库。 FetchContent 命令官方文档✅ 示例代码 我们将以 fmt 这个流行的格式化库为例,演示如何: 使用 FetchContent 从 GitH…...
uniapp中使用aixos 报错
问题: 在uniapp中使用aixos,运行后报如下错误: AxiosError: There is no suitable adapter to dispatch the request since : - adapter xhr is not supported by the environment - adapter http is not available in the build 解决方案&…...
3-11单元格区域边界定位(End属性)学习笔记
返回一个Range 对象,只读。该对象代表包含源区域的区域上端下端左端右端的最后一个单元格。等同于按键 End 向上键(End(xlUp))、End向下键(End(xlDown))、End向左键(End(xlToLeft)End向右键(End(xlToRight)) 注意:它移动的位置必须是相连的有内容的单元格…...
4. TypeScript 类型推断与类型组合
一、类型推断 (一) 什么是类型推断 TypeScript 的类型推断会根据变量、函数返回值、对象和数组的赋值和使用方式,自动确定它们的类型。 这一特性减少了显式类型注解的需要,在保持类型安全的同时简化了代码。通过分析上下文和初始值,TypeSc…...
Chromium 136 编译指南 Windows篇:depot_tools 配置与源码获取(二)
引言 工欲善其事,必先利其器。在完成了 Visual Studio 2022 和 Windows SDK 的安装后,我们即将接触到 Chromium 开发生态中最核心的工具——depot_tools。这个由 Google 精心打造的工具集,就像是连接开发者与 Chromium 庞大代码库的智能桥梁…...
【从零开始学习JVM | 第四篇】类加载器和双亲委派机制(高频面试题)
前言: 双亲委派机制对于面试这块来说非常重要,在实际开发中也是经常遇见需要打破双亲委派的需求,今天我们一起来探索一下什么是双亲委派机制,在此之前我们先介绍一下类的加载器。 目录 编辑 前言: 类加载器 1. …...
