多线程的学习中篇下

请添加图片描述

volatile 关键字

volatile 能保证内存可见性
volatile 修饰的变量, 能够保证 “内存可见性”

示例代码:

image-20230927234356426

运行结果:
image-20230927234904570

当输入1(1是非O)的时候,但是t1这个线程并沿有结束循环,
同时可以看到,t2这个线程已经执行完了,而t1线程还在继续循环.

这个情况,就叫做内存可见性问题 ~~ 这也是一个线程不安全问题(一个线程读,一个线程改)

while (myCounter.flag == 0) { // 循环体空着,什么也不做 }

这里使用汇编来理解,大概就是两步操作:

  1. load, 把内存中 flag 的值,读取到寄存器里.
  2. cmp, 把寄存器的值,和0进行比较,根据比较结果,决定下一步往哪个地方执行(条件跳转指令).

上述是个循环,这个循环执行速度极快,一秒钟执行百万次以上…
循环执行这么多次,在线程 t2 真正修改之前, load得到的结果都是一样的;
另一方面, load操作和cmp操作相比,速度慢非常非常多!!!

注:
CPU针对寄存器的操作,要比内存操作快很多,快3-4数量级;
计算机对于内存的操作,比硬盘快3-4个数量级.

由于 load 执行速度太慢(相比于cmp来说),再加上反复 load 到的结果都一样, JVM 就做出了一个非常大胆的决定 ~~ 判定好像没人改 flag 值,不再真正的重复 load 了,干脆就只读取一次就好了 => 编译器优化的一种方式.
实际上是有人在修改的,但是 JVM/编译器 对于这种多线程的情况,判定可能存在误差.
此时,就需要我们手动干预了,可以给 flag 这个变量加上 volatile 关键字,意思就是告诉编译器,这个变量是"易变"的,要每次都重新读取这个变量的内存内容,不能再进行激进的优化了.
博主感慨: 快和准之间往往不可兼得

内存可见性问题

一个线程针对一个变量进行读取操作,同时另一个线程针对这个变量进行修改,此时读到的值,不一定是修改之后的值;这个读线程没有感知到变量的变化;
归根结底是 编译器/JVM 在多线程环境下优化时产生了误判了.
备注:
(1)上述说的内存可见性编译器优化的问题,也不是始终会出现的(编译器可能存在误判,也不是100%就误判!);
(2)编译器的优化,很多时候是“玄学问题”,应用程序这个角度是无法感知的.编译器的优化,很多时候是“玄学问题”,应用程序这个角度是无法感知的.

代码改正:

class MyCounter {volatile public int flag = 0;
}

运行结果:

image-20230928004924362

注意事项:
(1) volatile 只能修饰变量;
(2) volatile 不能修饰方法的局部变量,局部变量只能在你当前线程里面用,不能多线程之间同时读取/修改(天然就规避了线程安全问题);

(1)局部变量只能在当前方法里使用的,出了方法变量就没了,方法内部的变量在"栈”这样的内存空间上;
(2)每个线程都有自己的栈空间,即使是同一个方法,在多个线程中被调用,这里的局部变量也会处在不同的栈空间中,本质上是不同变量,也就涉及不到修改/读取同一个变量的操作;
(3)栈记录了方法之间的调用关系;
个人理解: 局部变量只对当前线程可见,其他线程看不了.

(3) 如果一个变量在两个线程中,一个读,一个写,就需要考虑volatile 了;
(4) volatile 不保证原子性,原子性是靠 synchronized 来保证的. synchronized 和 volatile 都能保证线程安全 => 不能使用 volatile 处理两个线程并发++这样的问题;
(5) 如果涉及到某个代码,既需要考虑原子性,有需要考虑内存可见性,就把 synchronized 和 volatile 都用上就行了.


从 JMM 的角度重新表述内存可见性问题

内存可见性问题,其他的一些资料,谈到了JMM(Java Memory Mode ~~ Java内存模型)
从 JMM 的角度重新表述内存可见性问题(Java的官方文档的大概表述):
Java 程序里,主内存,每个线程还有自己的工作内存(线程 t1 的和线程 t2 的工作内存不是同一个东西);
线程 t1 进行读取的时候,只是读取了工作内存的值;
线程 t2进行修改的时候,先修改的工作内存的值,然后再把工作内存的内容同步到主内存中,但是由于编译器优化,导致线程 t1没有重新的从主内存同步数据到工作内存,读到的结果就是“修改之前"的结果.

如果把"主内存”代替成"内存",把“工作内存"代替成"CPU寄存器",就容易理解.
注: 之所以上面这段话这么晦涩,是翻译不行,翻译官得背锅 ~~ 翻译的结果让人误会了!!!
主内存: main memory => 主存,也就是平时所说的内存
工作内存: work memory =>工作存储区,并非是所说的内存,而是CPU上存储数据的单元(寄存器)

为什么Java这里,不直接叫做“CPU寄存器",而是专门搞了"工作内存”说法呢?

这里的工作内存,不一定只是CPU的寄存器,还可能包括CPU的缓存cache.

image-20230928013620585

当CPU要读取一个内存数据的时候,可能是直接读内存也可能是读cache还能是读寄存器…
引入cache之后,硬件结构就更复杂了,工作内存(工作存储区): CPU寄存器 + CPU的cache;
一方面是为了表述简单,另一方面也是为了避免涉及到硬件的细节和差异,Java里就使用"工作内存"这个词来统称(泛指)了;毕竟,现实中有的 CPU 可能没有 cache, 有的 CPU 有;有的 CPU 可能有一个cache,还可能有多个;现代的 CPU 普遍是3级cache, L1, L2, L3,总之,情况多样.
注: 学校的"计算机系统结构”会讲解CPU内部的结构,尤其是寄存器, cache,指令等等,上这门课的时候要好好听讲.

wait 和 notify

线程最大的问题,是抢占式执行,随机调度~~
程序猿写代码,不喜欢随机,喜欢确定的东西,于是发明了一些办法,来控制线程之间的执行顺序,虽然线程在内核里的调度是随机的,但是可以通过一些 API 让线程主动塞,主动放弃CPU(给别的线程让路).
比如,t1,t2 两个线程,希望t1先干活,干的差不多了,再让t2来干.就可以让t2先wait(阻塞,主动放弃CPU)等t1干的差不多了,再通过notify 通知t2,把t2唤醒,让t2接着干.

那么上述场景,使用 join 或者 sleep行不行呢?

使用join,则必须要t1彻底执行完,t2才能运行.如果是希望t1先干50%的活,就让t2开始行动,join无能为力.
使用sleep,指定一个休眠时间的,但是t1执行的这些活,到底花了多少时间,不好估计.
使用wait和notify可以更好的解决上述的问题.

注: wait, notify, notifyAll 这几个类,都是Object类(Java里所有类的祖宗)的方法.Java里随便new个对象,都可以有这三个方法!!

wait

wait 进行阻塞.某个线程调用wait方法,就会进入阻塞(无论是通过哪个对象 wait的),此时就处在WAITING状态.
wait 不加任何参数,就是一个"死等"一直等待,直到有其它线程唤醒它.
示例代码:

public class ThreadDemo16 {public static void main(String[] args) throws InterruptedException {Object object = new Object();object.wait();}
}

throws InterruptedException : 这个异常,很多带有阻塞功能的方法都带.这些方法都是可以 interrupt 方法通过这个异常给唤醒的.

运行结果:

image-20230927204141554

IllegalMonitorStateException
~~ 非法的锁状态异常
~~ 锁的状态,无非就是被加锁的状态和和被解锁的状态.

为什么有这个异常,要先理解 wait 的操作是干什么了.
1.先释放锁
2.进行阻塞等待
3.收到通知之后,重新尝试获取锁,并且在获取锁后,继续往下执行.

这里锁状态异常,就是没加锁呢,就想着释放锁.就好比单身着呢,就想着分手.

public static void main(String[] args) throws InterruptedException {Object object = new Object();synchronized (object) {System.out.println("wait 之前");object.wait();System.out.println("wait 之后");}}

image-20230927205029017

虽然这里wait是阻塞了,阻塞在 synchronized 代码块里,实际上,这里的阻塞是释放了锁的,此时其他线程是可以获取到object这个对象的锁的,此时这里的阻塞,就处在WAITING状态.

image-20230928094946950

t1.start();
t2.start();

如果代码这里写作 t1.start 和 t2.start 由于线程调度的不确定性,此时不能保证一定是先执行 wait ,后执行notify. 如果调用notify,此时没有线程wait,此处的wait是无法被唤醒的!!!(这种通知就是无效通知).
因此此处的代码还是要尽量保证先执行wait后执行notify才是有意义的.

改正的代码:

   public static void main(String[] args) throws InterruptedException {Object object = new Object();Thread t1 = new Thread(() -> {// 这个线程负责进行等待System.out.println("t1: wait 之前");try {synchronized (object) {object.wait();}} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t2: wait 之后");});Thread t2 = new Thread(() -> {System.out.println("t2: notify 之前");synchronized (object) {// notify 务必要获取到锁,才能进行通知object.notify();}System.out.println("t2: notify 之后");});t1.start();// 此处写的 sleep 500 是大概率会让 t1 先执行 wait 的// 极端情况下,电脑特别卡的时候, 可能线程的调度时间就超过了 500 ms// 还是可能 t2 先执行 notifyThread.sleep(500);t2.start();}

运行结果:

image-20230927214510375

此处,先执行了wait,很明显wait操作阻塞了,没有看到wait之后的打印;
接下来执行到了t2, t2进行了notify的时候,才会把t1的wait唤醒.t1才能继续执行.
只要t2不进行notify,此时t1就会始终wait下去(死等).

wait无参数版本,就是死等的.
wait带参数版本,指定了等待的最大时间.

wait的带有等待时间的版本,看起来就和sleep有点像.其实还是有本质差别的:
虽然都是能指定等待时间,也都能被提前唤醒(wait是使用notify 唤醒, sleep使用interrupt唤醒)但是这里表示的含义截然不同.
notify唤醒wait,这是不会有任何异常的.(正常的业务逻辑),interrupt唤醒sleep 则是出异常了(表示一个出问题了的逻辑).

如果当前有多个线程在等待object对象,此时有一个线程 object.notify(),此时是随机唤醒一个等待的线程.(不知道具体是哪个),但是,可以用多组不同的对象来控制线程的执行顺序.
比如,有三个线程,希望先执行线程1,再执行线程2,再执行线程3,
创建obj1,供线程1,2使用创建obj2,供线程2,3使用线程3, obj2.wait
线程2.obj1.wait(),唤醒之后执行obj2.notify()
线程1执行自己的任务,执行完了之后,obj1.notify即可.

notifyAll和notify非常相似.
多个线程 wait 的时候, notify随机唤醒一个, notifyAll 所有线程都唤醒,这些线程再一起竞争锁…

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

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

相关文章

再生之术:遗忘 Root 密码的 CentOS8 Stream 解决方案

文章目录 大魔头 RootGRUB 引导界面BootLoaderGRUB主要功能选择启动的操作系统编辑内核启动参数 进入GRUB 引导界面编辑内核启动参数单用户模式 进入内核编辑界面rd.break进入单用户模式 大魔头 Root 哈哈,你好!今天,让我们来聊聊 Linux 系统…

ElementPlus· tab切换/标签切换 + 分页

tab切换 ---> <el-tabs><el-tab-pane>... 分页 --------> <el-pagination> tab切换 // tab标签切换 // v-model双向绑定选项中的name&#xff0c;tab-change事件在 activeName改变时触发 <script setup> const tabChange (tab, event)>{…

PostgreSQL 数据库实现公网远程连接

文章目录 前言1. 安装postgreSQL2. 本地连接postgreSQL3. Windows 安装 cpolar4. 配置postgreSQL公网地址5. 公网postgreSQL访问6. 固定连接公网地址7. postgreSQL固定地址连接测试 前言 PostgreSQL是一个功能非常强大的关系型数据库管理系统&#xff08;RDBMS&#xff09;,下…

微信小程序 预约系统

目录 前端介绍主要页面介绍主页面签到相关页面个人中心扫描页面工作页面 技术栈说明 后端介绍技术栈说明 前端介绍 主要页面介绍 主页面 这个页面主要是一个轮播图加上三个小按钮和一个海报&#xff0c;具体可以看代码 签到相关页面 这一些列图片展示了&#xff0c;签到的流…

基于springboot实现二手交易平台管理系统演示【项目源码】分享

基于springboot实现二手交易平台管理系统演示 java简介 Java语言是在二十世纪末由Sun公司发布的&#xff0c;而且公开源代码&#xff0c;这一优点吸引了许多世界各地优秀的编程爱好者&#xff0c;也使得他们开发出当时一款又一款经典好玩的小游戏。Java语言是纯面向对象语言之…

加速企业AI实施:成功策略和效率方法

文章目录 写在前面面临的挑战MlOps简介好书推荐 写作末尾 写在前面 作为计算机科学领域的一个关键分支&#xff0c;机器学习在当今人工智能领域中占据着至关重要的地位&#xff0c;广受瞩目。机器学习通过深入分析大规模数据并总结其中的规律&#xff0c;为我们提供了解决许多…

【C++】STL之list深度剖析及模拟实现

目录 前言 一、list 的使用 1、构造函数 2、迭代器 3、增删查改 4、其他函数使用 二、list 的模拟实现 1、节点的创建 2、push_back 和 push_front 3、普通迭代器 4、const 迭代器 5、增删查改(insert、erase、pop_back、pop_front) 6、构造函数和析构函数 6.1、默认构造…

java常用API之Object

Objct toString() package myObjct;public class myObjct {public static void main(String[] args) {Object onew Object();System.out.println(o.toString());//打印结果java.lang.Object27f674d} }java.lang.Object27f674d后面的27f674d是地址值 package myObjct;import ja…

2022年软件设计师下半年真题解析(上午+下午)

1 RISC 以下关于RISC(精简指令集计算机)特点的叙述中&#xff0c;错误的是()。 A.对存储器操作进行限制&#xff0c;使控制简单化B.指令种类多&#xff0c;指令功能强 C.设置大量通用寄存器 D.选取使用频率较高的一些指令&#xff0c;提高执行速度 RISC(Reduced Instruction Se…

油猴(篡改猴)学习记录

第一个Hello World 注意点:默认只匹配了http网站,如果需要https网站,需要自己添加match https://*/*代码如下 这样子访问任意网站就可以输出Hello World // UserScript // name 第一个脚本 // namespace http://tampermonkey.net/ // version 0.1 // descri…

Flask扩展:简化开发的利器以及26个日常高效开发的第三方模块(库/插件)清单和特点总结

目录 寻找扩展 使用扩展 创建扩展 26个常用的Flask扩展模块 总结 原文&#xff1a;Flask扩展&#xff1a;简化开发的利器以及26个日常高效开发的第三方模块&#xff08;库/插件&#xff09;清单和特点总结 (qq.com) Flask是一个轻量级的Python Web框架&#xff0c;它提供…

数据结构--栈

线性表的定义 前面文章有讲过&#xff0c;线性表就是一次保存单个同类型元素&#xff0c;多个元素之间逻辑上连续 例子&#xff1a;数组&#xff0c;栈&#xff0c;队列&#xff0c;字符串 栈 1.1 栈和队列的特点 栈和队列都是操作受限的线性表。 前面学过的数组&#xff0c;…

Cocos Creator3.8 实战问题(一)cocos creator prefab 无法显示内容

问题描述&#xff1a; cocos creator prefab 无法显示内容&#xff0c; 或者只显示一部分内容。 creator编辑器中能看见&#xff1a; 预览时&#xff0c;看不见内容&#xff1a; **问题原因&#xff1a;** prefab node 所在的layer&#xff0c;默认是default。 解决方法&…

wps及word通配匹配与正则匹配之异同

前言 今天在chatgpt上找找有什么比赛可以参加。下面是它给我的部分答案&#xff0c;我想将其制成文档裱起来&#xff0c;并突出比赛名方便日后查找。 这时理所当然地想到了查找替换功能&#xff0c;但是当我启用时却发现正则匹配居然没有了&#xff0c;现在只有通配匹配了。 …

关于接口测试——自动化框架的设计与实现

一、自动化测试框架 在大部分测试人员眼中只要沾上“框架”&#xff0c;就感觉非常神秘&#xff0c;非常遥远。大家之所以觉得复杂&#xff0c;是因为落地运用起来很复杂&#xff1b;每个公司&#xff0c;每个业务及产品线的业务流程都不一样&#xff0c;所以就导致了“自动化…

从零开始之了解电机及其控制(11)实现空间矢量调制

广泛地说&#xff0c;空间矢量调制只是将电压矢量以及磁场矢量在空间中调制到任意角度&#xff0c;通常同时最大限度地利用整个电压范围。 其他空间矢量调制模式确实存在&#xff0c;并且根据您最关心的内容&#xff0c;它们可能值得研究。 如何实际执行这种所谓的交替反向序列…

java进阶-Netty

Netty 在此非常感谢尚硅谷学院以及韩顺平老师在B站公开课 Netty视频教程 Netty demo代码文件 I/O 说NIO之前先说一下BIO&#xff08;Blocking IO&#xff09;,如何理解这个Blocking呢&#xff1f;客户端监听&#xff08;Listen&#xff09;时&#xff0c;Accept是阻塞的&…

XML文件反序列化读取

原始XML文件 <?xml version"1.0" encoding"utf-8" ?> <School headmaster"王校长"><Grade grade"12" teacher"张老师"><Student name"小米" age"18"/><Student name&quo…

freertos的任务调度器的启动函数分析(根据源码使用)

volatile uint8_t * const pucFirstUserPriorityRegister ( uint8_t * ) ( portNVIC_IP_REGISTERS_OFFSET_16 portFIRST_USER_INTERRUPT_NUMBER ); 通过宏pucFirstUserPriorityRegister0xE000E400&#xff08;根据宏名字&#xff0c;这是NVIC寄存器地址&#xff09; 查手册…

服务器补丁管理软件

随着漏洞的不断上升&#xff0c;服务器修补是增强企业网络安全的典型特征。作为业务关键型机器&#xff0c;计划服务器维护的停机时间无疑是一件麻烦事。但是&#xff0c;借助高效的服务器补丁管理软件&#xff08;如 Patch Manager Plus&#xff09;&#xff0c;管理员可以利用…