线程的一些事(2)
在java中,线程的终止,是一种“软性”操作,必须要对应的线程配合,才能把终止落实下去
然而,系统原生的api其实还提供了,强制终止线程的操作,无论线程执行到哪,都能强行把这个线程干掉。
这样的操作Java的api中没有提供的,上述的做法弊大于利,强行取结束一个线程,很可能线程执行到一半,会出现一些残留的临时性质的“错误”数据。
public class ThreadDemo12 {public static void main(String[] args) {boolean isQuit = false;Thread t = new Thread(() -> {while (!isQuit){System.out.println("我是一个线程,工作中!!");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}//当前是死循环,给了个错误指示/* System.out.println("线程工作完毕!");*/});t.start();try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("让t线程结束!");isQuit = true;}
}
我们将变量isQuit作为main方法中的局部变量。
弹出了警告,这就涉及到lambda表达式的变量捕获了,当前捕获的变量是isQuit所以对于isQuit来说,它要么加上final,要么不去进行修改。
isQuit是局部变量的时候,是属于main方法的栈帧中,但是Thread lambda是又自己独立的栈帧的,这两个栈帧的生命周期是不一致的
这就可能导致main方法执行完了,栈帧就销毁了,同时Thread的栈帧还在,还想继续使用isQuit--
在java中,变量捕获的本质就是传参,就是让lambda表达式在自己的栈帧创建一个新的isQuit并把外面的isQuit的值拷贝过来(为了避免isQuit的值不同步,java就不让isQuit来进行修改)
等待线程
多个线程的执行顺序是随机的,虽然线程的调度是无序的,但是可以通过一些api来影响线程执行的顺序。
join就可以,
public class ThreadDemo14 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("我是一个线程,正在工作中...");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("线程执行结束");});t.start();/* Thread.sleep(5000);*///这个操作就是线程等待t.join();System.out.println("这是主线程,期望这个日志在 t 结束后打印");}
}
这种方法比sleep方法要好很多,毕竟谁也不知道t线程啥时候结束,用join可以让线程等 t 线程结束后再执行,这时候main线程的状态就是“阻塞”状态了。
Thread类基本的使用
1.启动线程 start方法
理解 run 和 start 区别
2.终止线程 核心让run方法能够快速结束
非常依赖 run 内部的代码逻辑
Thread isInterrupted(判定标志位)/interrupt(设置标志位)
如果提前唤醒sleep会清楚标志位
3.等待线程 join 让一个线程等待另一个线程结束
线程之间的顺序我们无法控制,但我们可以控制结束顺序
获取线程引用
Thread.currentThread()获取到当前线程的 引用(Thread 的引用)
如果是继承Thread,直接使用 this 拿到线程实例
如果不是则需要使用 Thread.currentThread();
线程的状态
就绪:这个线程随时可以去 cpu 上执行
阻塞:这个线程暂时不方便去cpu上执行
java中线程又以下几种状态:
1.NEW Thread 对象创建好了,但是还没有调用 start 方法在系统中创建线程.
2.TERMINATED Thread 对象仍然存在,但是系统内部的线程已经执行完毕了
3.RUNNABLE 就绪状态.表示这个线程正在 cpu 上执行,或者准备就绪随时可以去 cpu 上执行4.TIMED WAITING 指定时间的阻塞, 就在到达一定时间之后自动解除阻塞.使用 sleep 会进入这个状态.使用带有超时时间的join也会
5.WAITING 不带时间的阻塞 (死等),必须要满足一定的条件,才会解除阻塞
6.BLOCKED 由于锁竞争,引起的阻塞,
线程安全问题,来看下面的一段代码
public class ThreadDemo19 {private static int count = 0;public static void main(String[] args) throws InterruptedException {//随便创建个对象都行/* Object locker = new Object();*///创建两个线程,每个线程都针对上述 count 变量循环自增 5w次Thread t1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {/* synchronized(locker) {count++;}*/count++;}});Thread t2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {/* synchronized(locker) {count++;}*/count++;}});t1.start();t2.start();t1.join();t2.join();//打印count结果System.out.println("count = " + count);}
}
我们发现这个结果是错的,我们计算的结果应该是100000.
这就涉及到线程安全问题了
count++是由三个指令构成的
1.load 从内存中读取数据到cpu寄存器
2.add 把寄存器中的值 + 1
3.save 把寄存器的值写回到内存中
对于单个线程是没有这种问题的,但是对于多线程就会冒出来问题
我们发现预期是进行两次count++后返回的count为2,但是因为两个线程在读取时出现了问题,第二个线程读取的数据是还未进行更新的数据,这就导致出现了错误。
如果是这样的顺序自然没有问题了
我们需要的进行顺序应该时等第一个线程save后第二个线程再进行load。
本质时因为线程之间的调度时无序的时抢占式执行
这就不得不提到String这个“不可变对象”了
1.方便JVM进行缓存(放到字符串常量池中)
2.hash值固定
3.线程安全的
线程不安全原因
1.根本原因 操作系统上的线程时“抢占式执行”“随即调度” => 线程之间执行顺序带来了很多变数
2.代码结构 代码中多个线程,同时修改同一个变量
1.一个线程修改一个变量
2.多个线程读取同一个变量
3.多个线程修改不同变量
这些都不会有事
3.直接原因 上述的线程修改操作本身不是’原子的‘
4.内存可见性问题
5.指令重排序问题
对于3这个问题我们可以找办法来解决
1.对于抢占式执行修改,这是无法改变的事
2.对代码结构进行调整,这是个办法,但在有些情况下也是不适用的
3.可以通过特殊手段将着三个指令打包为一个“整体”,我们可以对其进行加锁
加锁
目的:把三个操作,打包成一个原子操作
进行加锁的时候需要先准备好锁对象,一个线程针对一个锁对象加锁后,当其他线程对锁对象进行加锁,则会产生阻塞(BLOCKED)(锁冲突/锁竞争),一直到前一个线程释放锁为止
要加锁得用到synchronized。
进入()就会加锁(lock),出了{ }就会解锁(unlock),synchronnized 是调用系统的 api 进行加锁,系统api本质上是靠 cpu 上特定指令完成加锁
当t1加锁后,在没解锁的情况下,t2再想进行加锁就会出现阻塞
在t1没有解锁的情况下,即使t1被调度出cpu,t2也还是在阻塞
即使这样会影响到执行效率,但也比串行要快不少。
我们只是对count加锁使得count串行,但for循环还是可以进行“并发”执行的
加锁之后结果就正确了。
对于对象的话只要不是同一个对象就不会有竞争这一说。
1.如果一个线程加锁,一个不加,是不会出现锁竞争的
2.如果两个线程,针对不同的对象加锁,也还是会存在线程安全问题
3.
把count放到一个 Test t 对象中,通过add来修改锁对象的时候可以写作this
相当于给this加锁(锁对象 this)
对于静态方法的话相当于给类对象加锁
我们可不可以加两个锁呢?
是否会打印hello?
为啥会打印成功?不应该出现锁冲突吗?
当前由于是同一个线程,此时锁对象,就知道了第二次加锁的线程,就是持有锁的线程,第二次操作,就可以直接放行不会出现阻塞。
这个特性被称为“可重入”
一旦上述的代码出现了阻塞,就称为“死锁”
可重入锁就是为了防止我们在“不小心”中引入的问题
当我们在第一次加锁的时候,计数器会进行加一操作,当第二次进行加锁的时候,大仙加锁的线程和持有锁线程是一个线程,这个时候就会加锁成功,并且计数器加一。
等到了计数器为0的时候才是真正的解锁了,对于可重入锁来说:
1.当前这个锁是被哪个线程持有的
2.加锁次数的计数器
计数器可以帮助线程清楚的记录有几个锁。
加锁能够解决线程安全问题,但同时也引入了一个新的问题就是死锁。
死锁的三种典型场景
1.一个线程一把锁
如果锁是不可重入锁,并且对一个线程对这把锁进行加锁两次
2.两个线程,两把锁
线程 1 获得 锁A
线程 2 获得 锁B
接下来 1 尝试获取B, 2 尝试获取 A就同样出现死锁了!!!
一旦出现“死锁”,线程就“卡住了”无法继续工作
public class ThreadDemo22 {public static void main(String[] args) {Object A = new Object();Object B = new Object();Thread t1 = new Thread(() -> {synchronized (A){//sleep一下是给t2时间让t2也能拿到Btry {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}//尝试获取B,并没有释放Asynchronized (B){System.out.println("t1拿到两把锁");}}});Thread t2 = new Thread(() -> {synchronized (B){//sllep一下,是给t1时间,让t1能拿到Atry {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}//尝试获取A并没有获取Bsynchronized (A){System.out.println("t2拿到了两把锁");}}});t1.start();t2.start();}
}
就像这样。
3.N个线程M把锁
哲学家就餐
解决死锁问题的方案
产生死锁的四个必要条件
1.互斥使用,获取锁的过程是互斥的,一个线程拿到了这把锁,另一个线程也想获取,就需要阻塞等待。
2.不可抢占,一个线程拿到了锁之后,只能主动解锁,不能让别的线程强行把锁抢走。
3.请求保持,一个线程拿到了锁 A 之后,在持有A的前提下,尝试获取B
4.循环等待,环路等待
由于四个都是必要条件,所以只要破环一个就解决问题了。
1,2.锁最为基本的特性
3.代码结构要看实际需求
4.代码结构的,最为容易破坏
指定一定的规则,就可以有效的避免循环等待
1.引入额外的筷子
2.去掉一个线程
3.引入计数器,限制最多同时所少人吃饭
4.引入加锁顺序的规则
内存可见性引起的线程安全问题
public class ThreadDemo23 {private static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {while(flag == 0){}System.out.println("t1线程结束");});Thread t2 = new Thread(() -> {System.out.println("请输入flag的值:");Scanner scanner = new Scanner(System.in);flag = scanner.nextInt();});t1.start();t2.start();}
}
运行代码发现,并没有我们想象的打印t1线程结束,而是直接不动了。
在这个过程中有两个关键的点
1.load 操作执行的结果,每次都是一样的(要想输入,过几秒才能输入,在这几秒都不知道循环都已经执行了上百亿次了)
2.load 操作开销远远超过 条件跳转
访问寄存器的操作速度,远远超过访问内存
由于load开销大,并且load的结果又一直没有变化,所以jvm就会怀疑load操作有必要存在的必要吗?
此时jvm就可能做出代码优化,把上述load操作,给优化掉(只有前几次进行load,后续发现,load反正都一样,静态分析代码,也没看到哪里改了flag,因此就把load操作,干掉了),干掉之后,就相当于不再重复读内存直接使用寄存器之前”缓存“的值,大幅度的提高循环的执行速度
多线程的情况下很容易出现误判,这里相当于 t2 修改了内存,但是 t1 没有看到这个内存优化,就称为”内存可见性“问题
我们发现在刚刚的代码加上sleep就会执行成功,即使sleep时间有多小。 因为不加sleep一秒钟可能循环上百亿次,load开销非常大,优化迫切程度就更高。
加了sleep,一秒钟可能循环的次数就可能变为1000次,这样load开销相对来说就小了,所以优化迫切程度就想对来说就低了。
内存可见性问题,其实是个高度依赖编译器优化的问题,啥时候触发这个问题,都不知道
所以干脆希望不要出现内存可见性问题,将上述优化给关闭了
这就要使用关键字 volatile 来对上述的优化进行强制的关闭(虽然开销大了,效率低了。但是数据准去性/逻辑正确性提高了)。
volatile 关键字
核心功能就是保证内存可见性(另一个功能进制指令重排序)
在上述的代码中,编译器发现,每次循环都要读取内存,开销太大,于是就把读取内存操作优化成读取寄存器操作,提高效率
在JMM模型的表述下
在上述代码中,编译器发现,每次循环都要读取”主内存“,就会把数据从”主内存“中复制到”工作内存“中,后续每次都是读取”工作内存“。
相关文章:

线程的一些事(2)
在java中,线程的终止,是一种“软性”操作,必须要对应的线程配合,才能把终止落实下去 然而,系统原生的api其实还提供了,强制终止线程的操作,无论线程执行到哪,都能强行把这个线程干掉…...
数据可视化:艺术与科学的交汇点,如何让数据“开口说话”?
数据可视化:艺术与科学的交汇点,如何让数据“开口说话”? 数据可视化,是科技与艺术的结合,是让冰冷的数字变得生动有趣的桥梁。它既是科学——讲究准确性、逻辑性、数据处理的严谨性;又是艺术——强调美感…...

使用lldb看看Rust的HashMap
目录 前言 正文 读取桶的状态 获取键值对 键值对的指针地址 此时,读取数据 读取索引4的键值对 多添加几个键值对 使用i32作为键,&str作为值 使用i32作为键,String作为值 前言 前面使用ldb看了看不同的类型,这篇再使用…...
Oracle版本、补丁及升级(12)——版本体系
12.1. 版本体系 Oracle作为最流行的一款关系数据库软件产品,其拥有自己一套成熟的版本管理体系。具体版本体系以12c为分界线,前后版本体系分别不同。 12.1.1. 12c之前版本 12c之前的Oracle,版本共有5位阿拉伯数字组成,其中的每位数字,都有各自的含义,具…...

2025最新免费视频号下载工具!支持Win/Mac,一键解析原画质+封面
软件介绍 适用于Windows 2025 最新5月蝴蝶视频号下载工具,免费使用,无广告且免费,支持对原视频和封面进行解析下载,亲测可用,现在很多工具都失效了,难得的几款下载视频号工具,大家且用且珍…...
在 Ubuntu 中配置 Samba 实现「特定用户可写,其他用户只读」的共享目录
需求目标 所有认证用户可访问 Samba 共享目录 /path/to/home;**仅特定用户(如 developer)**拥有写权限;其他用户仅允许读取;禁止匿名访问。 配置步骤 1. 设置文件系统权限 将目录 /home3/guest 的所有权设为 develo…...
Newton GPU 机器人仿真器入门教程(零)— NVIDIA、DeepMind、Disney 联合推出
系列文章目录 目录 系列文章目录 前言 一、快速入门 1.1 实时渲染 1.2 USD 渲染 1.3 示例:创建一个粒子链 二、重要概念 三、API 参考 3.1 求解器 3.1.1 XPBD 求解器 3.1.2 VBD 求解器 3.1.3 MuJoCo 求解器 3.2 关节控制模式 四、Newton 集成 4.1 Is…...
《零基础学机器学习》学习大纲
《零基础学机器学习》学习大纲 《零基础学机器学习》采用对话体的形式,通过人物对话和故事讲解机器学习知识,使内容生动有趣、通俗易懂,降低了学习门槛,豆瓣高分9.1分,作者权威。 接下来的数篇文章,我将用…...
CSS 基础知识分享:从入门到注意事项
什么是CSS? CSS是用于描述HTML或XML文档呈现方式的语言。它控制网页的布局、颜色、字体等视觉表现,让内容与表现分离。 通俗的说,html是骨头,那么css就是他的画皮。 基本语法 CSS规则由两部分组成:选择器和声明块。…...
深入浅出理解JavaScript原型与原型链
先让我们结合生活案例理解原型原型链相关概念,想象一下一个大家庭,有很多成员。 1. 原型 (Prototype) - 家族的共同特征或技能模板 概念对应: 家族中代代相传的共同特征、习惯、或者家族里独有的某个手艺或知识。例子: 假设你们家族的成员普遍都有高个子、善于烹饪一道祖传菜…...
重操旧业,做起了OnlineTool.cc在线工具站
最近闲来无事,做了个在线工具站。 工具不多,起码有:当前IP查询,QRCode二维码生成,图片压缩,JSON格式化,简体繁体转换,等。 使用Astro框架React,Caddy,目前是…...
vue 中的数据代理
在 Vue 中,数据代理(Data Proxy) 是 Vue 实现 MVVM 模式 的关键技术之一。Vue 使用数据代理让你可以通过 this.message 访问 data.message,而不需要写 this.data.message —— 这大大简化了模板和逻辑代码。 我们来深入理解它的本…...
ubuntu安装Go SDK
# 下载最新版 Go 安装包(以 1.21.5 为例) wget https://golang.google.cn/dl/go1.21.5.linux-amd64.tar.gz # 解压到系统目录(需要 root 权限) sudo tar -C /usr/local -xzf go1.21.5.linux-amd64.tar.gz # 使用 Go 官方安装脚本…...

【C++】学习、项目时Debug总结
这里写目录标题 1. 内存问题1.1. 内存泄漏1.1.1. 内存泄漏案例检查方法1.1.2. 主线程提前退出导致【控】1.1.3. PostThreadMessage失败导致的内存泄漏**【控】**1.1.4. SendMessage 时关闭客户端【控】1.1.5. 线程机制导致【**控】**1.1.6. exit(0)导致【…...

26考研——中央处理器_指令流水线_指令流水线的基本概念 流水线的基本实现(5)
408答疑 文章目录 六、指令流水线指令流水线的基本概念流水线的基本实现流水线设计的原则流水线的逻辑结构流水线的时空图表示 八、参考资料鲍鱼科技课件26王道考研书 六、指令流水线 前面介绍的指令都是在单周期处理机中采用串行方法执行的,同一时刻 CPU 中只有一…...
Flutter——数据库Drift开发详细教程(三)
目录 参考正文核心API写入(更新、插入、删除)1.更新和删除2.使用 SQL 表达式更新3.插入件4.更新插入5.返回 参考 https://drift.simonbinder.eu/dart_api/writes/#updating-with-sql-expressions 正文核心API 写入(更新、插入、删除&#…...

AI Agent-基础认知与架构解析
定义 AI Agent 可以理解为一种具备感知、决策和行动能力的智能实体,能够在复杂的环境中自主运行,并根据环境变化动态调整自身行为,以实现特定目标。与传统的人工智能程序相比,AI Agent 具有更强的自主性、交互性和适应性。它不仅能…...
privateGPT和RAGflow之间的区别
PrivateGPT和RAGFlow都是基于RAG(检索增强生成)技术的开源项目,但它们在设计目标、技术架构和应用场景上有显著差异。以下是两者的详细对比分析: 1. 核心定位与设计目标 PrivateGPT 隐私优先:专注于完全离线的私有化部署,确保用户数据不离开本地环境,适合对隐私要求极高…...

C语言--字符函数
C语言--字符函数 一、字符函数1.1 iscntrl1.2 isspace1.3 isdigit1.4 isxdigit1.5 islower1.6 isupper1.7 isalpha1.8 isalnum1.9 ispunct1.10 isgraph1.11 isprint 在编程的过程中,我们会经常处理字符,为了方便操作,C语言标准库中提供了一系…...
Android对工程中的String中文字符的整理
本文主要介绍使用python快速整理工程中的中文字符,为app国际化提供便利。 1. 查找Android工程中的所有中文字符串(find_chinese.py) import os import re import argparsedef is_comment_line(line, file_ext):"""判断一行是否是注释:param lin…...

菜鸟之路Day30一一MySQL之DMLDQL
菜鸟之路Day30一一MySQL之DML&DQL 作者:blue 时间:2025.5.8 文章目录 菜鸟之路Day30一一MySQL之DML&DQL一.DML0.概述1.插入语句(insert)2.更新语句(update)3.删除语句(delete…...
集团云解决方案:集团企业IT基础架构的降本增效利器
在当今数字化飞速发展的时代,集团企业面临着诸多挑战,尤其是IT基础架构的管理和运营成本居高不下,效率却难以提升。别担心,集团云解决方案的出现为集团企业带来了全新的曙光,真正实现了降本增效! 一、集团…...

基 LabVIEW 的多轴电机控制系统
在工业自动化蓬勃发展的当下,多轴伺服电机控制系统的重要性与日俱增,广泛应用于众多领域。下面围绕基于 LabVIEW 开发的多轴伺服电机控制系统展开,详细阐述其应用情况。 一、应用领域与场景 在 3D 打印领域,该系统精确操控打印头…...
SD06_前后端分离项目部署流程(采用Nginx)
本文档详细描述了如何在Ubuntu 20.04服务器上从零开始部署Tlias前后端分离系统。Tlias系统由Spring Boot后端(tlias-web-management)和Vue前端(vue-tlias-management)组成。 目录 环境准备安装MySQL数据库部署后端项目部署前端项…...
【kubernetes】通过Sealos 命令行工具一键部署k8s集群
一、前言 1、sealos安装k8s集群官网:K8s > Quick-start > Deploy-kubernetes | Sealos Docs 2、本文安装的k8s版本为v1.28.9 3、以下是一些基本的安装要求: 每个集群节点应该有不同的主机名。主机名不要带下划线。所有节点的时间需要同步。需要…...

《Go小技巧易错点100例》第三十二篇
本期分享: 1.sync.Map的原理和使用方式 2.实现有序的Map sync.Map的原理和使用方式 sync.Map的底层结构是通过读写分离和无锁读设计实现高并发安全: 1)双存储结构: 包含原子化的 read(只读缓存,无锁快…...
怎么判断是不是公网IP?如何查看自己本地路由器是内网ip还是公网?
在网络世界中,IP 地址如同每台设备的 “门牌号”,起着至关重要的标识作用。而 IP 地址又分为公网 IP 和私网 IP,准确判断一个 IP 属于哪一类,对于网络管理、网络应用开发以及理解网络架构等都有着重要意义。接下来,我们…...
【上位机——MFC】单文档和多文档视图架构
单文档视图架构 特点:只能管理一个文档(只有一个文档类对象) #include <afxwin.h> #include "resource.h"//文档类 class CMyDoc :public CDocument {DECLARE_DYNCREATE(CMyDoc) //支持动态创建机制 }; IMPLEMENT_DYNCREATE(CMyDoc,CDocument) //…...

需求分析阶段测试工程师主要做哪些事情
在软件测试需求分析阶段,主要围绕确定测试范围、明确测试目标、细化测试内容等方面开展工作,为后续测试计划的制定、测试用例的设计以及测试执行提供清晰、准确的依据。以下是该阶段具体要做的事情: 1. 需求收集与整理 收集需求文档&#x…...
Web 实时通信技术:WebSocket 与 Server-Sent Events (SSE) 深入解析
一、WebSocket: (一)WebSocket 是什么? WebSocket 是一种网络通信协议,它提供了一种在单个 TCP 连接上进行全双工通信的方式。与传统的 HTTP 请求 - 响应模型不同,WebSocket 允许服务器和客户端在连接建立…...