多线程的学习中篇下

news2024/11/25 4:27:51

请添加图片描述

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 先执行 notify
        Thread.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.coloradmin.cn/o/1048704.html

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

相关文章

初识Java 11-2 函数式编程

目录 高阶函数 闭包 函数组合 柯里化和部分求值 本笔记参考自: 《On Java 中文版》 高阶函数 ||| 高阶函数的定义:一个能接受函数作为参数或能把函数当返回值的函数。 把函数当返回值的情况: import java.util.function.Function;inter…

26551-2011 畜牧机械 粗饲料切碎机

声明 本文是学习GB-T 26551-2011 畜牧机械 粗饲料切碎机. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本标准规定了粗饲料切碎机的产品型号、技术要求、试验方法、检验规则、标志、包装、运输与贮存。 本标准适用于加工农作物秸秆等粗饲料…

Python接口自动化之unittest单元测试

以下主要介绍unittest特性、运行流程及实际案例。 一、单元测试三连问 1、什么是单元测试? 按照阶段来分,一般就是单元测试,集成测试,系统测试,验收测试。单元测试是对单个模块、单个类或者单个函数进行测试。 将…

迅为龙芯2K1000开发板通过汇编控制GPIO

上一小节,我们使用了 C 语言控制了 gpio,这一小节我们来看一下如何使用汇编来控制 gpio 呢?有的 同学可能会有疑问了,既然我们可以使用 C 语言来控制 gpio,为什么我们还要使用更底层的汇编语言呢, 如果我…

【Read View】Read View如何在MVCC里面工作、事务的隔离级别如何实现等重点知识汇总

目录 Read View 在 MVCC 里如何工作的 隔离级别如何实现的呢 Read View 在 MVCC 里如何工作的 ReadView用于管理事务之间的数据一致性,特别是在并发访问数据库时。 那 Read View 到底是个什么东西? Read View 有四个重要的字段: m_ids &…

Python函数:chr()和ord()

两个函数是基于Unicode编码表进行进行字符与字码之间的转换。 chr()函数是通过字码转换成字符: 如图,坐标(1,4e10)丑 使用chr需要线将坐标相加得到:4e11 chr默认传入10进制的字码. 如图是各进制的字码。 也可以传入其他进制,不过需要在前面传入的参数最前…

canvas+javascript实现点击更改图片中指定部位的颜色(将近似的颜色值变更为自定义的颜色)

原图&#xff1a; 完整代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" co…

前端架构师之02_ES6_高级

1 类和继承 1.1 class类 JavaScript 语言中&#xff0c;生成实例对象的传统方法是通过构造函数。 // ES5 创建对象 // 创建一个类&#xff0c;用户名 密码 function User(name,pass){// 添加属性this.name name;this.pass pass; } // 用 原型 添加方法 User.prototype.sho…

跨境电商如何利用海外代理IP,提高竞争力?

随着经济全球化的深度发展&#xff0c;跨境电商已经成为外贸发展的主要赛道&#xff0c;跨境电商王者般的新业态&#xff0c;近年来&#xff0c;我国跨境电商发展得到政府的大力扶持。而代理IP也逐渐成为助力跨境业务的强大工具之一。可以为我们跨境电商种出现的如地域限制、安…

中秋国庆,双节同乐

中秋国庆双节将至&#xff0c;送上最诚挚的祝福&#xff1a; 愿你的中秋月圆&#xff0c;家庭团圆更圆满&#xff0c;幸福围绕&#xff0c;笑声不断。 愿国庆日祖国繁荣昌盛&#xff0c;人民幸福安康&#xff0c;和谐美好永恒。 让我们共同庆祝这个特殊的时刻&#xff0c;祝福您…

MDETR:端到端多模态理解的调制检测

代码&#xff1a;https://github.com/ashkamath/mdetr 摘要 多模态推理系统依靠预训练的目标检测器从图像中提取感兴趣的区域。然而&#xff0c;这个关键模块通常用作黑匣子&#xff0c;独立于下游任务进行训练&#xff0c;并使用固定的对象和属性词汇表。这使得此类系统难以捕…

【数据仓库设计基础(三)】数据集市

文章目录 一. 数据集市的概念二. 数据集市与数据仓库的区别三. 数据集市设计 一. 数据集市的概念 数据集市是数据仓库的一种简单形式&#xff0c;通常由组织内的业务部门自己建立和控制。一个数据集市面向单一主题域&#xff0c;如销售、财务、市场等。 数据集市的数据源可以是…

Lucene-MergePolicy详解

简介 该文章基于业务需求背景&#xff0c;因场景需求进行参数调优&#xff0c;下文会尽可能针对段合并策略&#xff08;SegmentMergePolicy&#xff09;的全参数进行说明。 主要介绍TieredMergePolicy&#xff0c;它是Lucene4以后的默认段的合并策略&#xff0c;之前采用的合并…

网络时钟程序Net_Clock

网络时钟程序Net_Clock 小程序的源代码,着重编程思想,巧妙的构思架构... 主要功能:简单的Socket网络套接从时间服务器获取时间,timeSetEvent精度定时器,颜色方案,自绘标题栏,自绘菜单...等等 VS2015平台的精度时钟程序,北京,美东,美中三个时区日期时间显示,多种颜色方案显示…

深入理解常见应用级算法思想

1 概论 1.1 概念 1.1.1 数据结构 1&#xff09;概述 数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下&#xff0c;精心选择的数据结构可以带来更高的运行或者存储效率。 2&#xff09;划分 从关注的维度…

洗文案神器,不同洗文案神器怎么选择

在数字时代&#xff0c;文字的重要性变得愈发突出&#xff0c;不论是在商业领域的广告宣传、自媒体的文章创作&#xff0c;还是学术界的研究论文&#xff0c;文字都是传递信息和思想的关键媒介。然而&#xff0c;创作高质量的文案并非易事&#xff0c;这就是为什么越来越多的人…

FFmpeg安装教程

一、下载安装 打开官方网站&#xff0c;ffmpeg管网地址&#xff0c;点击左侧download或页面中的绿色Download按钮。 选择要下载的平台&#xff0c;windows版本选择红框第一个&#xff0c;第二个是github平台 点击左侧release builds&#xff0c;选择要下载的软件版本&#x…

【STM32】sct 分散加载文件的格式与应用

简介 当工程按默认配置构建时&#xff0c;MDK 会根据我们选择的芯片型号&#xff0c;获知芯片的内部FLASH 及内部 SRAM 存储器概况&#xff0c;自动生成一个以工程名命名的后缀为*.sct 的分散加载文件(Linker Control File&#xff0c;scatter loading)&#xff0c;链接器根据…

巨人互动|Google海外户Google SEO工作为何要把控细节

Google SEO&#xff08;搜索引擎优化&#xff09;是一项为了提高网站在Google搜索结果中的排名和可见性的策略和技术。在进行SEO工作时&#xff0c;把控细节非常重要&#xff0c;本文小编讲讲关于为何要把控细节的原因。 巨人互动|Google海外户&Google内容定位介绍&#xf…

当当网商品详情数据接口

当当网商品详情数据接口可以通过当当网的开放平台获取相关信息。您可以注册当当开放平台账号&#xff0c;并按照要求提交申请获取API接口的调用凭证。获得授权后&#xff0c;您将会收到一组AccessKey和SecretKey。使用编程语言&#xff08;如Java&#xff09;调用API接口&#…