《深入理解JAVA虚拟机(第2版)》- 第12章 - 学习笔记

news2024/9/19 21:45:57

第12章 Java内存模型与线程

12.1 概述

TPS是用来衡量一个服务性能好坏高低的重要指标值。TPS是Transactions Per Second的缩写,用来表示每秒事务处理数,即服务端每秒平均能碰响应的请求数。

12.2 硬件的效率与一致性

处理器与内存的运算效率差了好几个数量级(处理器要比内存快的多的多),为了提高处理器的处理效率,诞生了高速缓存(Cache),它介于处理器与内存之间,运算的时候需要将数据从内存中加载到缓存中,运算结束后还需要将结果同步回内存中。通过高速缓存,处理器就不需要等待内存读取了。

虽然,处理器的处理效率提高了,但是在一个多处理器的系统中同时也带来一个新的问题——缓存一致性(Cache Coherence)。因为在一个多处理器的系统中,每个处理器都有一个与之对应的高速缓存,而它们又共享一个主内存(Main Memory)。

处理器、高速缓存、主内存间的交互关系,如下图:
在这里插入图片描述

12.3 Java内存模型

Java内存模型屏蔽了各种物理硬件和操作系统对内存访问的差异,使得Java程序能在不同的平台上达到一致的内存访问效果。

而C/C++是直接使用物理硬件和操作系统的内存模型,这导致有可能发生相同的程序在不同的平台上并发执行的结果不同(有的并发能顺利执行,有的并发则失败)。

12.3.1 主内存与工作内存
  1. Java内存模型的主要目标是规定程序中各个变量的访问规则,即在虚拟机中将变量存储到内存中和从内存中取出变量这样的底层细节。这里提到的变量包括,实例字段、静态字段和构成数组的元素,而不包括局部变量和方法参数,因为局部变量和方法参数都是线程私有的,不会被共享,也就不会存在竞争问题。

  2. Java内存模型规定所有变量都要存储在主内存(Main Memory,可以类比12.2中提到的硬件的主内存)中。每条线程有自己的工作内存(Working Memory,可以类比12.2中提到高速缓存),线程的工作内存中所保存的线程中所使用的变量实际上是主内存中该变量的一个副本,线程只能操作(读写)变量的副本,而不能直接操作(读写)主内存中的变量。

  3. 不同线程之间不能直接访问对方工作内存中的变量,要想实现线程间的变量值传递只能通过主内存来完成。

  4. 线程、工作内存、主内存三者的交互关系(与12.2中的处理器、高速缓存、主内存类似),如下图:
    在这里插入图片描述

12.3.2 内存间交互操作

这里要说的内存交互操作,实际就是主内存和工作线程之间的交互操作,即一个变量从主内存加载到工作内存中,或从工作内存同步回主内存中。

Java内存模型规定了8种操作来完成内存间的交互,这8中操作都是原子的、不可再分的(但是对于long和double类型的变量而言,在某些平台上read、load、write、store操作允许有了例外情况,具体介绍可跳到本文的【12.3.4】)。

  • lock(锁定)

    作用于主内存中的变量,将该变量标记为被某线程占有(即锁定)。

  • unlock(解锁)

    与lock相同,同样作用于主内存中的变量,将处于锁定状态的变量释放出来,释放后的变量才允许被其他线程再锁定。

  • read(读取)

    作用于主内存的变量,将主内存中变量的值传输到工作内存中。

  • load(载入)

    作用于工作内存的变量,将read进来的变量值存储到工作内存中的变量副本中。

  • use(使用)

    作用于工作内存中的变量,将工作内存中的变量值传递给执行引擎。

  • assign(赋值)

    作用于工作内存中的变量,将执行引擎接收到的变量值赋值给工作内存中的变量。

  • store(存储)

    作用于工作内存中的变量,将工作内存中的变量值传输到主内存中。

  • write(写入)

    作用于主内存中的变量,将store进来的变量值写入到主内存的变量中。

其中read、load这组操作是将一个变量从主内存中复制到工作内存中,而store、write这组操作是将工作内存中的变量同步回主内存中。这两组操作只要保证按顺序执行就行,没必要是连续的。拿read、load这组操作为例,如果对主内存中的a、b两个变量进行访问,指令的顺序有一种可能是:read-a、read-b、load-b、load-a。

12.3.3 对于Volatile型变量的特殊规则

关键字Volatile是Java虚拟机提供的最轻量级的同步机制。

被Volatile修饰的变量具有两个特性:

  • 保证该变量对所有线程的可见性

    这里所说的“可见性”是指当一个线程修改了该变量的值,新值对其他线程来说是可以立即得知的。

    下面提出两个问题:

    1. volatile变量在不同的线程中是一致的吗?

      :在各个线程的工作内存中,volatile变量也可能出现不一致的情况,但由于每次在使用(即use操作)之前都要先刷新下,执行引擎每次用的都是最新的变量值,因此可以认为不存在一致性的问题。

    2. 基于volatitle变量的运算在并发情况下是线程安全的吗?

      答:volatile变量只是能保证对所有线程的可见性,但是线程中的运算却不是原子操作,所以基于volatile变量的运算在并发的情况下是线程不安全的。为了帮助大家理解,请看下图: 在这里插入图片描述

    下面举一个使用volatile变量来控制并发的场景,如下:

    volatile boolean shutdownRequested;
    
    ​​public void shutdown() {
        shutdownRequested = true;
    }
    
    ​​public void doWork() {
        while (!shutdownRequested) {
            // do stuff
        }
    }​​
    

    从上边的代码我们可以看到,当一个线程执行了shutdown()方法,则其他所有正在执行doWork()的线程都将结束while循环。

  • 禁止指令重排序

    指令重排序从硬件架构上来讲,是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。

    指令重排序优化是机器级的优化操作,对应的是汇编代码

    指令重排序会给程序并发执行带来一些干扰,DCL(双重锁定检查)单例模式是一个很典型的例子,这里我推荐一篇Blog(星夜孤帆的《DCL单例模式》),大家可以去看一下,应该会对大家去理解禁止指令重排序有一些帮助(PS:重点看【二、DCL单例】最后部分)。

12.3.4 对于long和double型数据的特殊规定

上文中提到的Java内存模型要求lock、unlock、read、load、use、assign、store、write这8个操作都必须具有原子性。但是对于64位的数据类型long和double,模型特别定义了一条宽松的规定,即允许没有被volatile修饰的long和double类型的数据在进行read、load、store、write这4种操作的时候不必具有原子性(将64位的数据读写操作拆分为成2次32位的操作),这就是long和double的非原子性协定(Nonatomic Treatment of double and long Variables)

虽然有long和double非原子性协定存在,但是大家也不必担心(每次编写代码的时候还要给long和double类型的变量声明volatile),目前各平台下的商业虚拟机都会将64位数据的读写操作作为原子操作来对待

12.3.5 原子性、可见性与有序性

Java内存模型是围绕着在并发情况下如何处理原子性、可见性和有序性这三个特征来建立的。下面整理下有哪些操作实现了这三个特征。

  • 原子性(Atomicity)

    由Java内存模型直接保证的原子性变量操作包括:read、load、use、assign、store、write,基本上我们可以认为基本数据类型的读写操作都是原子性的(除long和double的非原子性协定以外)。

  • 可见性(Visibility)

    对可见性的定义已经在上文的【12.3.3】中提到了,这里就不在赘述了。

    这里谈下可见性是如何实现的?

    Java内存模型是通过将修改后的新值同步回主内存,在读取变量前刷新变量值(从主内存中)这种依赖主内存的方式来实现可见性的。无论普通变量也好还是volatile变量也好,都是如此。普通变量与volatile变量的区别在于,被volatile修饰的变量修改后会立即同步回主内存,每次使用变量的时候也会先立即刷新变量值。

    除了volatile以外,关键字synchronized和final也可以保证可见性。

    • synchronized的可见性是通过“对一个变量进行unlock操作之前,必须先把该变量同步回主内存(执行store、write操作)”这条规则获得的。
    • final的可见性是指变量在构造器中初始化完成,并且在构造器中并没有将“this”的引用传递出去(此时将this的引用传递出去,可能会导致其他线程通过该引用访问到“初始化了一半”的对象),那么在其他线程中就能看到final变量的值。
  • 有序性(Ordering)

    有序性在上文的【12.3.3】中介绍volatile的禁止指令重排序中有过介绍,这里也不再赘述了。

    除了volatile关键字以外,关键字synchronized同样也可以保证线程之间操作的有序性,volatile是通过它本身的语义(禁止指令重排序)来保证的,而synchronized则是通过“一个变量在同一时刻只允许在被一条线程lock”这条规则获得的。

12.3.6 先行发生原则

//

12.4 Java与线程

  • 并发不一定依赖多线程,例如:PHP就是多进程并发,在Java中,并发大多与线程有关。

  • 线程是比进程更轻量级的调度执行单位,线程可以共享进行的资源(内存地址、文件I/O),又可以作为CPU调度的基本单位被CPU独立调度。

  • 在Java中Thread类中,所有关键方法都是声明为Native的

  • 线程的实现(注意:这里没有特指Java线程的实现)有三种方式,如下:

    1. 基于内核线程实现

      首先说下什么是内核线程?

      内核线程(Kernel-Level Thread,KLT)是由操作系统内核(Kernel)支持的线程,内核通过操作调度器来完成线程调度,并将线程任务映射到各个处理器上。支持多线程的内核称为多线程内核。

      程序一般不会直接操作内核线程,而是通过内核线程的高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常所说的线程,每个轻量级进程都需要一个内核线程来支持,它们是1:1的关系,被称为一对一的线程模型,如下图: 在这里插入图片描述
      采用内核线程实现的线程以下两个缺点:

      • 所有线程的操作都需要进行系统调用,代价比较大,需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换。
      • 由于每个轻量级进程都需要对应一个内核线程,因此会消耗一定的内核资源(例如:内核线程的栈空间),因此系统支持轻量级进程的数量是有限的。
    2. 基于用户线程实现

      广义上来讲,所有非内核线程的线程,都属于用户线程(User Thread, UT),轻量级进程也算用户线程。

      狭义上来讲,用户线程是建立在用户空间的线程池中,内核无法感知线程的存在。线程的所有操作(建立、同步、销毁、调度)都只是在用户态中完成(不需要内核的帮助)

      进程与用户线程属于1:N的关系,这种关系称为1对多的线程模型,如下图: 在这里插入图片描述
      采用用户线程实现线程优缺点如下:

      • 由于不需要在用户态和内核态之间来回切换,所以操作可以非常快。
      • 由于不会占用内核资源,所以能支持更大规模的线程数。
      • 缺点是由于没有内核的支持,所以所有线程操作都需要用户程序自己来完成。
    3. 基于用户线程+轻量级进程实现

      在这种混合模式中,用户线程还是在用户空间的线程池中创建,而轻量级进程则作为用户线程和内核线程的桥梁

      用户线程与轻量级进程的数量比试不确定的,即为N:M的关系,这种关系称为多对多的线程模型,如下图: 在这里插入图片描述

  • Java线程的实现

    在JDK1.2之前,Java线程是基于用户线程来实现的。而在JDK1.2中,Java线程是基于操作系统的原生线程模型来实现的。

    对于SUN JDK来说,Windows和Linux平台上均是采用的一对一的线程模型,一条Java线程映射到一条轻量级进程之中。而在Solaris平台上,既有支持一对一的线程模型也同时支持多对多的线程模型。

  • Java线程调度

    所谓线程调度就是系统为线程分配处理器使用权的过程

    主要的调度方式分为两种,如下:

    1. 协同式线程调度(Cooperative Threads-Scheduling)

      线程的执行时间由线程自己来决定,执行完之后,通知系统切换到另外一个线程上。

      优点:实现简单,切换对线程来说是完全可知的,所以不存在线程同步的问题。

      缺点:如果线程出了问题,无法通知系统切换,则会造成程序一直阻塞在那里。

    2. 抢占式线程调度(Preemptive Threads-Scheduling)

      线程的执行时间和线程的切换都是由系统来决定。

      在这种调度方式下,由于线程的执行时间是可控的,就不会造成一个线程导致整个进程阻塞的问题。

      Java采用的就是抢占式线程调度

  • 状态转换

    Java语言定义了6种现成的状态,如下:

    1. 新建

    2. 运行

    3. 无限期等待

      处于此状态的线程不会被分配CPU执行时间,只能等待被其他线程显性的唤醒。

    4. 限期等待

      处于此状态的线程也不会被分配CPU执行时间,它等到一定时间后,会被系统自动唤醒。

    5. 阻塞

      阻塞和等待的区别在于:阻塞是等待获取一个排它锁;而等待就是等待一段时间或者唤醒动作。

    6. 终止

    线程状态转换关系如下图:
    在这里插入图片描述

上一篇:《深入理解JAVA虚拟机(第2版)》- 第11章 - 学习笔记
下一篇:《深入理解JAVA虚拟机(第2版)》- 第13章 - 学习笔记

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

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

相关文章

使用阿里OCR身份证识别

1、开通服务 免费试用 2、获取accesskay AccessKeyId和AccessKeySecret 要同时复制保存下来 因为后面好像看不AccessKeySecret了 3.Api 参考 https://help.aliyun.com/zh/ocr/developer-reference/api-ocr-api-2021-07-07-recognizeidcard?spma2c4g.11186623.0.0.7a9f4b1e5C…

园区网基础组网保姆级(mstp,vrrp,irf,eth-trunk,route-policy,ospf,bgp,rbm,nat,mlag等等)

本文实验使用模拟器:H3C HCL 5.10.2版本 一、园区核心/接入架构1.1.三层架构1.2.二层架构二、园区核心 To 接入实践2.1.MSTP+VRRP派系2.1.1.MSTP+VRRP配置2.1.2.MSTP+VRRP验证2.2.IRF+Eth-Trunk派系2.2.1.IRF+Eth-Trunk配置2.3.两种派系的对比2.4.VXLAN结构三、园区核心/出口架…

观《中国数据库前世今生》有感:从历史中汲取未来的力量

观《中国数据库前世今生》有感:从历史中汲取未来的力量 中国数据库技术的起步与发展 观看了《中国数据库前世今生》后,我对于中国数据库技术的历史变迁有了更深刻的理解。作为一名有一年开发经验的程序员,这部纪录片让我对中国数据库行业从8…

828华为云征文 | 云服务器Flexus X实例,搭建上线前后端项目

828华为云征文 | 云服务器Flexus X实例,搭建上线前后端项目 项目搭建 演示使用华为云服务器Flexus X实例搭建上线前后端项目黑马vue电商后台管理系统 项目GitHub链接:https://github.com/Minori-ty/vue_shop.git 1、购买华为云 Flexus X 实例 Flexus云服…

.Net Core 生成管理员权限的应用程序

创建一个ASP.NET Core Web API项目 给解决方案设置一个名称 选择一个目标框架,这里选择的是 .NET 8.0框架 在Porperties文件夹中添加一个app.manifest文件 设置app.manifest文件属性,生成操作设置为嵌入的资源 双击解决方案名称,编辑WebAppli…

JS实现树形结构数据中特定节点及其子节点显示属性设置的技巧(可用于树形节点过滤筛选)

大家好,今天我要分享的是如何在树形结构的数据中,根据特定条件设置节点及其所有子节点的显示属性。在实际项目中,这种需求非常常见,特别是在需要动态展示和隐藏节点的情况下。下面我将通过一个具体的示例来讲解实现过程。 需求分析…

(史上最全)线程池

线程池 文章目录 线程池一,前言二,线程池三,参数四,线程池的实现原理5.线程池的使用案例(自定义线程池)6.使用Executors 创建常见的功能线程池1.固定大小线程池2.定时线程3.可缓存线程池4.单线程化线程池 一,前言 虽然…

Prometheus 上手指南

文章目录 Prometheus 相关概念Prometheus 的特点Prometheus 架构数据模型 Datemode使用场景 指标类型 Metric type适用场景 作业和实例 Jobs and instances使用场景 Prometheus 安装Prometheus 配置prometheusalertmanager Grafana 可视化Grafana 安装Grafana 配置选项Grafana …

假期学习--iOS 编译链接

iOS 编译链接 编译流程 四步: 1.预处理 2.编译 3.汇编 4.链接 大概的步骤如下: 预处理 作为编译的第一步,将.m文件转换为.i文件 ; 预处理是要处理源代码中所有以#开头的所有预编译指令 ; 规则如下&#xff1…

更换UFS绑定固件与“工程固件”的区别 小米10s机型更换cpu绑定包对比 写入以及修复基带

目前机型的安全机制越来越高。机型cpu与字库存在绑定关系。主板cpu如损坏需要更换。换新cpu后就需要刷写底层绑定包来修复 。今天的博文将为大家带来UFS绑定包与工程固件的区别以及写入 修复基带的步骤解析 通过博文了解 1💝💝💝-----更换UFS绑定包与工程固件的区别 2…

利士策分享,赚钱与体重:一场关于生活平衡的微妙探索

利士策分享,赚钱与体重:一场关于生活平衡的微妙探索 在当今社会,赚钱与体重,这两个看似风马牛不相及的概念, 却在无形中交织着人们的生活轨迹。 它们不仅仅是数字上的增减,更是个人选择、生活方式乃至心理…

Mycat搭建分库分表

分库分表解决的问题 单表数据量过大带来的性能和存储容量的限制的问题: 索引效率下降读写瓶颈存储容量限制事务性能问题分库分表架构 再搭建一对主从复制节点,3307主节点,3309从节点配置数据源 dw1 , dr1,创建集群c1创建逻辑库 CREATE DATAB…

【AI小项目6】QLoRA针对资源受限设备上的大模型微调和文本分类任务的推理

目录 一、项目简介概述时间主要工作和收获技术栈数据集结果参考 二、训练阶段的完整代码什么是 QLoRA 微调?注意 安装库导入包配置定义一个Config类配置训练参数配置Lora 初始化分词器和模型数据预处理计算模型评估指标交叉验证划分数据集举例: 创建Trai…

Redis-分片集群

分片集群 主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决: 海量数据存储问题 高并发写的问题 使用分片集群可以解决上述问题,如图: 分片集群特征: 集群中有多个master,每个master保存不同数据 每个ma…

Java-测试-Mockito 入门篇

之前很长一段时间我都认为测试就是使用SpringBootTest类似下面的写法&#xff1a; SpringBootTest class SysAuthServiceTest {AutowiredSysRoleAuthMapper sysRoleAuthMapper;Testpublic void test() {QueryWrapper<SysRoleAuth> queryWrapper new QueryWrapper<&g…

Linux命令:对文本文件的内容进行排序的工具sort详解

目录 一、概述 二、用法 1、 基本语法 2、 常用选项 3、获取帮助 三、示例 1. 基本用法 2. 按数字排序 3. 按第二列排序 4. 逆序排序 5. 删除重复行 6. 忽略大小写排序 7. 按人类可读的数值排序 8. 按版本号排序 四、高级用法 1. 与 uniq 结合使用去重 2. 与 gr…

1.使用 VSCode 过程中的英语积累 - File 菜单(每一次重点积累 5 个单词)

前言 学习可以不局限于传统的书籍和课堂&#xff0c;各种生活的元素也都可以做为我们的学习对象&#xff0c;本文将利用 VSCode 页面上的各种英文元素来做英语的积累&#xff0c;如此做有 3 大利 这些软件在我们工作中是时时刻刻接触的&#xff0c;借此做英语积累再合适不过&a…

不断挑战才有不断机遇!Eagle Trader等你来加入!

2024“Eagle Trader杯”全国职业交易联赛S1赛季已火热进行一个多月&#xff0c;吸引了超过355名交易员的积极参与&#xff01;目前&#xff0c;每天都有新的交易员踊跃报名参加&#xff01; 经过严格地交易考核&#xff0c;13名选手成功通过初试&#xff0c;正进入下一阶段的挑…

XILINX ZYNQ 7000 使用 FreeRTOS

XILINX 官方的SDK可以生成FreeRTOS 本文分为三个部分&#xff1a; 1.ZYNQ 7010 创建一个最小ZYNQ Processer系统&#xff0c;能够使用串口打印 2.使用SDK 创建一个FreeRTOS最小软件系统 3.浅析FreeRTOS最小软件系统 一&#xff1a;ZYNQ 7010 创建一个最小ZYNQ Processer系统&…

基于Linux系统离线安装oracle数据库

注意事项&#xff1a; 在安装的时候多次涉及root用户和oracle用户的切换&#xff0c;请注意区分&#xff0c;本文已明显 一、环境准备 1、关闭防火墙 [rootlocalhost ~]# systemctl stop firewalld2、 禁用NetworkManager服务&#xff08;非必须&#xff09; [rootlocalhost …