多线程的学习中篇下

请添加图片描述

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.mzph.cn/news/89935.shtml

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

相关文章

Java8实战-总结37

Java8实战-总结37 默认方法不断演进的 API初始版本的 API第二版 API 默认方法 传统上,Java程序的接口是将相关方法按照约定组合到一起的方式。实现接口的类必须为接口中定义的每个方法提供一个实现,或者从父类中继承它的实现。但是,一旦类库…

数据库:Hive转Presto(二)

继续上节代码,补充了replace_func函数,并加入一些except的判断。 import re import os from tkinter import *class Hive2Presto:def __int__(self):self.t_funcs [substr, nvl, substring, unix_timestamp] \[to_date, concat, sum, avg, abs, year,…

Qt扩展-KDDockWidgets 的使用

KDDockWidgets 的使用 一、概述二、原理说明三、代码实例1. 项目简述2. 布局源码 一、概述 KDDockWidgets 的使用相对比较简单,建议直接参考 其提供的例子。 二、原理说明 在这种多窗口布局显示的使用最常用的就是这两个类, 也就是 MainWindow 和 Doc…

MySQL 开启配置binlog以及通过binlog恢复数据

目录 一、binlog日志基本概念二、开启binlog日志记录2.1、查看binlog日志记录启用状态2.2、开启配置binlog日志 三、制作测试数据(可以先不执行,这里是为后续数据恢复做准备,先看数据恢复流程)四、使用binlog日志恢复数据4.1、前置…

中秋特辑——3D动态礼盒贺卡(可监听鼠标移动)

前言 「作者主页」:雪碧有白泡泡 「个人网站」:雪碧的个人网站 「推荐专栏」: ★java一站式服务 ★ ★ React从入门到精通★ ★前端炫酷代码分享 ★ ★ 从0到英雄,vue成神之路★ ★ uniapp-从构建到提升★ ★ 从0到英雄&#xff…

ROS的通信机制

ROS是一个分布式框架,为用户提供多节点(进程)之间的通信服务,所有软件功能和工 具都建立在这种分布式通信机制上,所以ROS的通信机制是最底层也是最核心的技术。在大多数应用场景下,尽管我们不需要关注底层通…

7.网络原理之TCP_IP(下)

文章目录 4.传输层重点协议4.1TCP协议4.1.1TCP协议段格式4.1.2TCP原理4.1.2.1确认应答机制 ACK(安全机制)4.1.2.2超时重传机制(安全机制)4.1.2.3连接管理机制(安全机制)4.1.2.4滑动窗口(效率机制…

在服务器上搭建pulseaudio的运行环境,指定其运行目录、状态目录和模块目录

如果想在搭建 PulseAudio 的服务器上指定其运行目录、状态目录和模块目录,可以通过修改 PulseAudio 的配置文件来实现。一般情况下所涉及的配置文件和相关选项如下所示: 1、配置文件路径:通常情况下,PulseAudio 的配置文件位于 /…

软件测试面试经验分享,真实面试题

前言 本人普通本科计算机专业,做测试也有3年的时间了,讲下我的经历,我刚毕业就进了一个小自研薪资还不错,有10.5k(个人觉得我很优秀),在里面呆了两年,积累了一些的经验和技能&#…

Flink Batch SQL Improvements on Lakehouse

本文整理自阿里云研发工程师刘大龙(风离),在 Streaming Lakehouse Meetup 的分享。内容主要分为三个部分: Flink Batch on Paimon 挑战Flink Batch 核心优化后续规划 点击查看原文视频 & 演讲PPT 一、Flink Batch on Paimon 挑…

andriod import 因为版本原因的替换

import androidx.core.app.ActivityCompat; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AlertDialog; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView;

启动 React APP 后经历了哪些过程

本文作者为 360 奇舞团前端开发工程师 前言 本文中使用的React版本为18,在摘取代码的过程中删减了部分代码,具体以源代码为准。 在React 18里,通过ReactDOM.createRoot创建根节点。并且通过调用原型链上的render来渲染。 本文主要是从以下两个…

Spring实现简单的Bean容器

1.BeanDefinition,用于定义 Bean 实例化信息,现在的实现是以一个 Object 存放对象 public class BeanDefinition {/*** bean对象*/private Object bean;/*** 存放 (定义)Bean 对象*/public BeanDefinition(Object bean) {this.bea…

长期用眼不再怕!NineData SQL 窗口支持深色模式

您有没有尝试过被明亮的显示器闪瞎眼的经历? 在夜间或低光环境下,明亮的界面会导致许多用眼健康问题,例如长时间使用导致的眼睛疲劳、干涩和不适感,同时夜间还可能会抑制褪黑素分泌,给您的睡眠质量带来影响。 这些问…

​比特币ETF将迎来审核窗口期

作者:Greg Cipolaro,NYDIG 全球研究主管 编译:WEEX Exchange 几只重要的 ETF 申请将于 10 月中旬迎来审核窗口,本文通过观察近期期权市场的动态,以研究交易者对这些关键 ETF 日期的仓位态度;门头沟&#xf…

Vue3 + TS 自动检测线上环境 —— 版本热更新提醒

🐔 前期回顾 编写 loading、加密解密 发布NPM依赖包,并实施落地使用_彩色之外的博客-CSDN博客 目录 🌍 问题产生 🤖 性能效率 🪂 新建 autoUpdate.ts 🎋 在App.vue使用 🌍 问题产生 当用…

《扩散模型 从原理到实战》Hugging Face (二)

第二章 Hugging Face简介 本章无有效内容 第三章 从零开始搭建扩散模型 有时候,只考虑事情最简单的情况反而更有助于理解其工作原理。本章尝试从零开始搭建廓庵模型,我们将从一个简单的扩散模型讲起,了解其不同部分的工作原理,…

IntelliJ IDEA快速查询maven依赖关系

1.在Maven窗口中点击Dependencies->show Dependencies 2.得到依赖关系图 此时原有快捷键Ctrlf可以查询jar包,如果没有查询菜单出来则设置快捷键方式为 File->Settings->Keymap->搜索栏输入find->在Main Menu下Edit下Find下Find双击算则Add keyboard…

云可观测性安全平台——掌动智能

云可观测性安全平台是一个跨架构、跨平台的可观测性方案,实现对云环境下的细粒度数据可视化,满足安全部门对云内部安全领域的多场景诉求,包括敏感数据动态监管、云网攻击回溯分析、攻击横移风险监控、云异常流量分析。本文将介绍掌动智能云可…

Oracle 11g_FusionOS_安装文档

同事让安装数据库,查询服务器信息发现操作系统是超聚变根据华为openEuler操作系统更改的自研操作系统,安装过程中踩坑不少,最后在超聚变厂商的技术支持下安装成功,步骤可参数该文。 一、 安装环境准备 1.1 软件下载 下载地址:…