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

第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.mfbz.cn/a/879703.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

关于STM32项目面试题02:ADC与DAC篇(输入部分NTC、AV:0-5V、AI:4-20mA和DAC的两个引脚)

博客的风格是:答案一定不能在问题的后面,要自己想、自己背;回答都是最精简、最精简、最精简,可能就几个字,你要自己自信的展开。 面试官01:什么是模数转换/ADC?说说模数转换的流程? …

数字自然资源领域的实现路径

在数字化浪潮的推动下,自然资源的管理与利用正经历着前所未有的变革。本文将从测绘地理信息与遥感专业的角度,深度分析数字自然资源领域的实现路径。 1. 基础数据的数字化 数字自然资源的构建,首先需要实现基础数据的数字化。这包括地形地貌…

【速成Redis】02 Redis 五大基本数据类型常用命令

前言: 上一节课,我们对redis进行了初步了解,和安装好了redis。【速成Redis】01 Redis简介及windows上如何安装redishttps://blog.csdn.net/weixin_71246590/article/details/142319358?spm1001.2014.3001.5501 该篇博客,我们正…

八股文-JVM

是什么?有什么用?谁发明的?什么时候发明的? Java虚拟机,用来运行Java程序,有很多个版本的虚拟机,比如HotSpot,最开始是SUN公司开发人员,和Java一起发布,现在…

9. 什么是 Beam Search?深入理解模型生成策略

是不是总感觉很熟悉?Beam Search 是生成任务中常用的一种方法。 在之前第5,7,8篇文章中,我们都曾经用到过与它相关的参数,而对于早就有着实操经验的同学们,想必见到的更多。这篇文章将从示例到数学原理和代…

【C语言二级考试】循环结构设计

C语言二级考试——循环结构程序设计 五.循环结构程序设计 1.for循环结构 2.while和do-while循环结构 3.continue语句和break语句 4.循环的嵌套 知识点参考【C语言】循环-CSDN博客 文章目录 1.for循环2.while和do-while循环结构3.continue语句和break语句4.循环的嵌套 1.for循环…

智谱清影 -CogVideoX-2b-部署与使用,带你揭秘生成6s视频的极致体验!

文章目录 1 效果展示2 CogVideoX 前世今生3 CogVideoX 部署实践流程3.1 创建丹摩实例3.2 配置环境和依赖3.3 模型与配置文件3.4 运行4 遇到问题 1 效果展示 A street artist, clad in a worn-out denim jacket and a colorful bandana, stands before a vast concrete wall in …

论文速递!时序预测!DCSDNet:双卷积季节性分解网络,应用于天然气消费预测过程

本期推文将介绍一种新的时序预测方法:双卷积季节性分解网络(Dual Convolution withSeasonal Decomposition Network, DCSDNet)在天然气消费预测的应用,这项研究发表于《Applied Energy》期刊。 针对天然气消费的多重季节性和非规律性&#x…

C++ —— 关于vector

目录 链接 1. vector的定义 2. vector的构造 3. vector 的遍历 4. vector 的扩容机制 5. vector 的空间接口 5.1 resize 接口 5.2 push_back 5.3 insert 5.4 erase 5.5 流插入与流提取 vector 并不支持流插入与流提取,但是可以自己设计,更…

标准库标头 <barrier>(C++20)学习

此头文件是线程支持库的一部分。 类模板 std::barrier 提供一种线程协调机制,阻塞已知大小的线程组直至该组中的所有线程到达该屏障。不同于 std::latch,屏障是可重用的:一旦到达的线程组被解除阻塞,即可重用同一屏障。与 std::l…

基于SpringBoot项目实现Docker容器化部署

将Spring Boot项目部署到Docker容器中的涉及几个主要步骤: 准备Docker镜像 首先,需要选择一个基础镜像,通常是包含Java运行时环境的镜像,例如OpenJDK。可以从Docker Hub或其他镜像仓库中获取这些镜像。接下来,需要在…

C++库文件移植到QT中一直出错

🏆本文收录于《CSDN问答解惑-专业版》专栏,主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收…

【软考】数据字典(DD)

目录 1. 说明2. 数据字典的内容2.1 说明2.2 数据流条目2.3 数据存储条目2.4 数据项条目2.5 基本加工条目 3. 数据词典管理4. 加工逻辑的描述4.1 说明4.2 结构化语言4.3 判定表4.3 判定树 5. 例题5.1 例题1 1. 说明 1.数据流图描述了系统的分解,但没有对图中各成分进…

一个基于 laravel 和 amis 开发的后台框架, 友好的组件使用体验,可轻松实现复杂页面(附源码)

前言 随着互联网应用的发展,后台管理系统的复杂度不断增加,对于开发者而言,既要系统的功能完备,又要追求开发效率的提升。然而,传统的开发方式往往会导致大量的重复劳动,尤其是在构建复杂的管理页面时。有…

【移动端开发】“明日头条APP”

文章目录 1 系统概述1.1研究背景1.2研究意义 2 系统设计2.1 关键技术2.2 系统设计2.2.1 系统功能模块2.2.2 数据库设计 3 系统实现3.1 数据模型3.1.1 NewsURL3.1.2 NewsType3.1.3 NewsInfo 3.2 数据库操作3.2.1 DBOpenHelper3.2.2 DBManager 3.3 适配器类3.3.1 AddItem3.3.2 In…

Redhat 7,8,9系(复刻系列) 一键部署Oracle19c rpm

Oracle19c前言 Oracle 19c 是甲骨文公司推出的一款企业级关系数据库管理系统,它带来了许多新的功能和改进,使得数据库管理更加高效、安全和可靠。以下是关于 Oracle 19c 的详细介绍: 主要新特性 多租户架构:支持多租户架构,允许多个独立的数据库实例在同一个物理服务器上…

【机器学习】9 ——最大熵模型的直观理解

机器学习9 ——最大熵模型的直观理解 文章目录 机器学习9 ——最大熵模型的直观理解前奏例子硬币垃圾邮件代码 前奏 【机器学习】6 ——最大熵模型 例子 硬币 假设我们有一枚硬币,可能是公平的,也可能是不公平的。我们的任务是估计硬币的正反面出现的…

通过Python代码发送量化交易信号邮件通知

量化交易利用数学模型和计算机算法来分析市场数据,并生成交易信号,本文将介绍如何使用Python编写一个简单的脚本,通过发送邮件通知量化交易信号。 开启SMTP服务 首先要在发件箱的邮件设置中,将POP3/SMPT服务开启,记录下授权密码,在本地可通过此密码登录,注意有效期和保…

微信小程序页面制作——婚礼邀请函(含代码)

✅作者简介:2022年博客新星 第八。热爱国学的Java后端开发者,修心和技术同步精进。 🍎个人主页:Java Fans的博客 🍊个人信条:不迁怒,不贰过。小知识,大智慧。 💞当前专栏…

【网络】TCP/IP 五层网络模型:网络层

最核心的就是 IP 协议,是一个相当复杂的协议 TCP 详细展开讲解,是因为 TCP 确实在开发中非常关键,经常用到,IP 则不同,和普通程序猿联系比较浅。和专门开发网络的程序猿联系比较紧密(开发路由器&#xff0…