【Java技术专题】「入门到精通系列教程」深入探索Java特性中并发编程体系的原理和实战开发指南(内存模型技术专题)

news2024/11/20 11:24:26

深入探索Java特性中并发编程体系的原理和实战开发指南( 线程进阶技术专题)

  • 前言介绍
  • JVM内存模型
  • 运行时数据区域
    • 堆内存
    • 栈内存
  • 内存访问规则
    • 原子性
      • 对象类型
      • 基本类型
    • 可见性
    • 有序性(Happen Before法则)
    • 系统内存(MESI协议)
  • 内存栅栏
    • 查看JIT编译结果
      • 这行配置的含义如下
    • 缓存行对齐
      • **缓存行对齐**
      • **伪共享**
  • 线程安全策略
    • 不可变类
    • 线程栈内使用
    • 同步锁
    • CAS (CompreAndSet)
  • 编程建议指南

前言介绍

JVM内存模型是Java程序实现线程安全和并发性的重要基础,对于Java程序员来说必须深入理解其中的原理和细节,才能有效避免由JVM内部的并发问题导致的程序错误和性能问题。

JVM内存模型

JVM内存模型是Java虚拟机规范中定义的一种用于管理内存使用的模型,主要分为两个部分:

  • 运行时数据区域:是Java程序运行时需要使用的内存区域,包括堆内存、栈内存、本地方法栈、方法区和程序计数器等。

  • 内存访问规则:是Java程序中多线程访问共享数据时所需要遵守的规则,包括原子性、可见性和有序性。其中:
    在这里插入图片描述

运行时数据区域

堆内存

在Java中,所有的对象实例的属性都存储在共享堆内存空间中。这个空间被单字节对齐,保证了内存的高效使用。需要注意的是,short类型的属性在堆内存中是无法被改变的。

栈内存

每个线程都有自己独立的线程栈空间,线程栈只存储基本数据类型和对象的地址。线程栈内存是4字节对齐且short型会被转化为int型。对象的地址长度为4字节且存储在引用堆空间中。方法内的局部变量存储在线程栈空间中,不会发生竞争,因此是线程安全的。方法的参数在栈顶交错存储,而不是被拷贝到栈顶寄存器中,这减少了中间状态的读取,同时也可以记录当前执行位置的PC指针。

内存访问规则

在这里插入图片描述

原子性

对象类型

  • 对象地址的原子读写是线程安全的: 在Java中,对象地址的读写是原子性的,并且具有线程安全性。这意味着多个线程可以同时读取或写入对象的地址,而不会导致数据竞争或内存不一致问题。

  • 对于不可变状态的并发读取是线程安全的: 如果对象在运行时保持其状态不变,那么多个线程同时读取它的状态是安全的。这个时候,读取操作之间没有任何干扰和依赖关系,并且可以自由地共享访问。因此,这种情况下是线程安全的。

  • 对于可变状态的并发读写不是线程安全的: 如果一个对象在运行时可以被多个线程同时读取和写入,那么在并发访问的情况下,就会发生数据竞争和内存不一致问题。这时候需要采取线程同步的措施来保证线程安全。例如,可以使用synchronized或Lock等机制来确保同时只有一个线程在执行对对象的读取或写入操作。

基本类型

  • 对于int和char类型的数值读写是线程安全的: 在Java中,int和char数据类型的读写操作是原子性的,因此具有线程安全性。这意味着多个线程可以同时读取或写入int和char类型的变量,而不会导致数据竞争或内存不一致的问题。

  • 对于long和double类型的高低位读写并不是线程安全的: 在Java中,long和double数据类型的高低位读写是非原子性的,这意味着在多个线程同时对一个long或double进行读写时,可能导致数据竞争和内存不一致的问题。为了确保线程安全性,需要采用同步机制来解决这个问题。

  • i++等组合操作不是线程安全的: i++等组合操作包含读写两个操作,并且具有非原子性,因此在多线程并发执行时是非线程安全的。这意味着在多个线程对同一个变量执行i++等组合操作时,可能导致数据竞争和内存不一致的问题。为了确保线程安全性,需要采用同步机制来解决这个问题。例如,可以使用synchronized或AtomicInteger等线程安全类来进行同步控制。

可见性

下面是对你提供的内容的润色和优化:

  • final关键字可以确保final字段的可见性: 当一个final字段被初始化后,其值不能再被修改,这意味着在多个线程间访问final字段时不会出现内存不一致的问题。同时,在Java语言规范中,final字段的初始化具有内存屏障的作用,确保了final字段初始化后对其他线程的可见性。

  • volatile关键字可以确保volatile字段的可见性: 通过使用volatile关键字声明的变量,每次读写时都会对内存进行同步操作,确保了变量的可见性。这意味着当一个线程修改了volatile变量的值后,其他线程能够立即看到该变量新的值,而不会看到可见性问题引起的数据不一致。

  • synchronized关键字可以确保同步块内读写字段的可见性: 在一个synchronized块中,当一个线程对某个字段进行写操作时,会立即将其刷新到主存储器中,同时其他线程在进入该synchronized块时,会首先尝试从主存储器中获取最新的字段值,从而确保了字段的可见性和一致性。

  • happen-before规则可以确保遵守happen-before次序的可见性: happen-before规则是Java内存模型中的一组规则,可以确保多个线程间的内存可见性。其中,如果一个操作happen-before另一个操作,那么前一个操作的结果对于后一个操作来说是可见的。在Java中,例如synchronized同步块、volatile变量的读写、启动线程和join线程等操作都是基于happen-before规则的,可以确保多个线程之间的内存可见性。

有序性(Happen Before法则)

  1. 程序次序法则: 如果A发生在B之前,则A和B之间具有happen before关系。

  2. 监视器法则: 监视器的解锁一定发生在后续对同一监视器加锁之前。

  3. Volatile变量法则: 写入volatile变量一定发生在后续对它的读取之前。

  4. 线程启动法则: 线程中的所有动作一定发生在Thread.start方法之前。

  5. 线程终结法则: 其他线程检测到某一线程已经终止,从Thread.join调用成功返回,或Thread.isAlive()返回false的发生一定在该线程中的所有动作之前。

  6. 中断法则: 一个线程调用另一个线程的interrupt方法一定发生在另一个线程检测到中断之前。

  7. 终结法则: 一个对象的构造函数结束一定发生在对象的finalizer方法之前。

  8. 传递性法则: 如果A发生在B之前,B发生在C之前,那么A一定发生在C之前。

系统内存(MESI协议)

Modified
本CPU写,则直接写到Cache,不产生总线事务;其它CPU写,则不涉及本CPU的Cache,其它CPU读,则本CPU需要把Cache line中的数据提供给它,而不是让它去读内存。
Exclusive
只有本CPU有该内存的Cache,而且和内存一致。 本CPU的写操作会导致转到Modified状态。
Shared
多个CPU都对该内存有Cache,而且内容一致。任何一个CPU写自己的这个Cache都必须通知其它的CPU。
Invalid
一旦Cache line进入这个状态,CPU读数据就必须发出总线事务,从内存读。

在这里插入图片描述

内存栅栏

volatile int a, b; if(a == 1 && b == 2)

JIT通过load acquire依赖保证读顺序:

0x2000000001de819c:  adds r37=597,r36;;  ;...84112554
0x2000000001de81a0:  ld1.acq r38=[r37];;  ;...0b30014a a010

volatile A a; a = new A();

JIT通过lock addl使CPU的cache line失效:

0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: lock addl $0x0,(%esp);

查看JIT编译结果

java -XX:+UnlockDiagnosticVMOptions -XX:PrintAssemblyOptions=hsdis-print-bytes -XX:CompileCommand=print,*AtomicInteger.incrementAndGet

这行配置的含义如下

  • java:代表要启动 Java 虚拟机以执行 Java 代码。
  • -XX:+UnlockDiagnosticVMOptions:打开 JVM 的诊断选项,该选项允许开发人员使用需要特权的命令。
  • -XX:PrintAssemblyOptions=hsdis-print-bytes:使用 HSDis(HotSpot Disassembler)打印本机代码的字节表示。这可以用于调试,分析代码优化和性能问题。
  • -XX:CompileCommand=print,*AtomicInteger.incrementAndGet:当使用 JIT 编译器编译调用 AtomicInteger.incrementAndGet() 方法的代码时,打印代码的汇编输出。

综上所述,这条配置命令允许开发人员在 JVM 中启用诊断选项并使用 HotSpot Disassembler 打印 Java 代码编译成的机器码汇编输出。其中,通过 -XX:CompileCommand=print,*AtomicInteger.incrementAndGet 捕捉了 AtomicInteger.incrementAndGet() 这个方法的编译过程,可以分析该方法对应的本地代码的汇编输出,这对于调试和分析性能问题非常有用。

缓存行对齐

缓存行对齐和伪共享都是与CPU缓存有关的概念。缓存是小型且从主内存中读取和写入数据比内存操作更快的内存。缓存行大小通常是64字节。当多个线程或处理器核心在操作共享变量时,缓存就会成为一个问题。

缓存行对齐

每个缓存行保存着多个数据元素。如果两个数据元素在同一个缓存行中,它们会在同一时刻被加载到CPU缓存中,这样就可以提高程序的性能。因此,缓存行对齐是在我们能够控制的数据元素之间添加填充以使它们位于不同的缓存行中。对于Java,可以使用 sun.misc.Contended 注解来实现线程对齐。

LinkedTransferQueue
static final class PaddedAtomicReference <T> extends AtomicReference <T> {
    Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;
    PaddedAtomicReference(T r) {
        super(r);
    }
}

16个地址的长度,刚好占满一个cache line的长度。确保两个引用,不在同一cache line上,防止多锁竞争。

伪共享

当两个线程完全独立但共享同一个缓存行中的不同数据元素时,可能会发生伪共享。操作其中一个数据元素会导致整个缓存行从内存中加载到CPU缓存,这会导致另一个线程无法访问该缓存行。这种现象称为“伪共享”,它会导致性能下降。

为了解决伪共享问题,可以使用一些技术来提高线程之间的独立性,例如将共享的数据分离到单独的缓存行中或者使用volatile变量或者Atomic方式。此外,可以使用一些特殊的注解来告诉编译器和运行时环境我们希望它们处理伪共享,例如Java 8开始引入的 sun.misc.Contended 注解。

线程安全策略

不可变类

如果一个类初始化后,所有属性和类都是final不可变的,这确实可以增加线程安全性,可以避免一些并发问题,例如多个线程同时修改同一对象可能出现的数据不一致等。

然而,即使这个类没有显式的同步,也不能保证它的线程安全,因为final关键字只能保证对对象的引用不变,而不能保证对象本身的线程安全性。如果这个类的方法没有进行同步,需要访问的对象的状态可能会发生变化,从而导致并发问题,例如读取脏数据、重复数据、遗漏数据等。

因此,即使一个类所有属性和类都是final不可变的,也不能保证这个类的线程安全,需要视具体情况进行合适的同步或其他线程安全措施。只有在确保访问对象的方法也是线程安全的情况下,才能认为这个类是完全线程安全的。

线程栈内使用

涉及多线程应用程序时,有几种方法可以提高应用程序的性能和可维护性,分别是方法内局部变量使用、线程内参数传递和使用ThreadLocal持有变量。

  1. 方法内局部变量使用

对于方法中仅在方法内被使用的变量,应该将其声明为局部变量,而不是作为全局变量存储在堆上。这样可以减少对象的创建数量,从而降低垃圾回收的负载,提高代码执行效率。

  1. 线程内参数传递

在多线程应用程序中,当一个方法需要访问某个对象时,最好将对象引用传递到方法中,而不是将对象作为全局变量。这样可以避免多个线程同时修改同一个对象的情况,从而提高应用程序的线程安全性。

  1. ThreadLocal持有变量

在线程之间传递数据时,如果不希望将数据暴露给其他线程,可以将数据存储在ThreadLocal对象中。ThreadLocal是Java中的一种线程范围内的数据结构,可以用于在不同的线程中存储和获取对象的值,而不必担心多个线程之间干扰。

综上所述,方法内局部变量的使用、线程内参数传递和使用ThreadLocal都是优化多线程应用程序的有效方法。在编写代码时,应综合考虑以上因素,以提高代码的执行效率和可维护性。

同步锁

  • 使用synchronized关键字锁定的代码会保证在同一时刻只有一个线程可以访问共享资源,因此它具有较高的线程安全性。但是,由于每个线程都必须等待前一个线程完成它的工作后才能继续执行,因此活性较低。
  • volatile变量可以确保可见性和禁止重排序,但只能在一些有限的情况下使用。可以使用锁外双重检测来尽可能地减少锁竞争,提高程序的性能。需要注意的是,对于访问次数较少的变量,使用volatile变量作为同步锁可能会影响程序的性能,因为锁的开销相对较大。
  • 读写条件分离、锁粒度分级和排序锁等技术可以降低锁的竞争,提高程序的性能。读写条件分离指的是将对共享资源的读和写操作分别加锁,从而允许多个线程并发地进行读操作。锁粒度分级指的是根据数据结构的特点,将锁的粒度分为不同的级别,避免过度细粒度的锁导致的锁竞争。排序锁则是对锁进行排序,以避免死锁和饥饿问题。这些技术需要根据具体情况进行灵活应用,以达到最优的性能和线程安全性的平衡。

CAS (CompreAndSet)

这里描述了一种基于“冲突检测与重试”的乐观并发方案。在这种方案中,每次更新操作时,使用比较并交换(CAS)指令判断当前值是否与期望值相等,若相等,则更新为新值;否则,表示中途有其他线程修改过该值,需要重新读取值并重试操作。

这种方案的优点是在没有竞争的情况下,可以快速地进行操作,提高程序的性能,并且不会发生死锁和饥饿问题。缺点是在竞争情况下,需要频繁地进行重试操作,消耗较多的CPU资源,并且可能导致进程的长时间阻塞,因此需要根据具体应用情况进行评估。

需要注意的是,乐观并发方案适用于不需要特别强的一致性要求,且数据冲突发生的概率较低的场景,例如计数器等任务。如果数据冲突较为频繁,建议采用悲观并发方案(例如使用锁进行同步),以保证数据的安全性和一致性。

编程建议指南

在编写代码时,需要考虑以下问题:

  1. 敲每个点号时,是否会出现空指针异常?

  2. 是否会有异常抛出?

  3. 代码是否在热点区域?

  4. 代码是在哪个线程执行?

  5. 是否存在并发锁的间隙?

  6. 是否会并发修改不可见?

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

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

相关文章

【OS】【期末选择题】【2023春】【仅供参考】

文章目录 题型一、选择第一章(10)第二章(19)第三章(23)第四章(32)第五章(15)第六章(15) 二、填空题三、简答题1.信号量2.调度算法3.页面置换4.虚拟地址到物理地址的映射 Reference 题型 题型题量分值选择10%填空25%10%10%解答题210’大题215’ 一、选择 第一章(10) 操作系统…

HuggingFace-RL-Unit2-Part1——Q-learning算法介绍

Q-learning算法介绍 文章目录 Q-learning算法介绍回顾: 什么是RL? 两种基于价值的方法状态价值函数动作价值函数 贝尔曼方程&#xff1a;简化价值计算蒙特卡罗 VS 时序差分学习蒙特卡洛&#xff1a;在一个回合结束后进行学习时序差分算法&#xff1a;在每一步进行学习 学习进展…

定时器的实现原理

文章目录 1.定时器的作用?2.数据结构要求3.时间轮4.分级时间轮5.业界实现方案参考文献 1.定时器的作用? 定时器的主要用途是执行定时任务。 定时任务在很多场景都需要用到&#xff0c;比如游戏的 Buff 实现&#xff0c;Redis 中的过期任务&#xff0c;Linux 中的定时任务&a…

java——多线程

文章目录 Java 的并发基础知识1. 创建线程2. 同步方法和同步代码块3. 线程安全的容器4. volatile 关键字5. Lock 和 Condition 接口 Java 多线程编程的基本框架1. 创建和启动线程2. 线程的状态转换3. 线程安全4. 死锁 Java 并发编程的高级技术1. 线程池2. 并发集合3. 原子类4. …

测试:进阶篇

在本篇章开始之前&#xff0c;先对之前的内容进行一个简单的总结回顾一下&#xff1a; 基于需求设计测试用例&#xff0c;这里有个测试用例的万能公式&#xff1a; 功能&#xff08;如果是软件&#xff0c;需要参考依据需求规格说明书&#xff1b;如果是物体&#xff0c;这个…

2023年7月电脑选择

文章目录 一、笔记本1.1 确定需求1.2 确定预算1.3 性能指标1.4 其他 二、台式电脑 最近有朋友让我推荐一下能做3D建模的笔记本电脑&#xff0c;本文就讲一下台式机和笔记本怎么选择。 一、笔记本 1.1 确定需求 当我们在选择笔记本时&#xff0c;首先需要确定自己的需求&#x…

CSS基础学习--24 表单

一、一个表单案例&#xff0c;我们使用 CSS 来渲染 HTML 的表单元素 <!DOCTYPE html> <html> <head> <meta charset"utf-8"> <title>CSS基础学习-表单</title> </head> <style> input[typetext], select {width:…

计算机基础--->数据结构(3)【堆(超详细)】

文章目录 堆堆的时间复杂度堆的分类堆的存储堆的操作插入元素删除堆顶元素 堆排序建堆排序 所有操作代码 堆 堆一般分为两种类型&#xff1a;最大堆和最小堆。在最大堆中&#xff0c;父节点的值总是大于或等于子节点的值&#xff0c;而在最小堆中&#xff0c;父节点的值总是小于…

计算机自我介绍500字范文(合集)

计算机自我介绍500字范文1 本人是___大学计算机系统维护专业的学生。大学四年的学习&#xff0c;积累了丰富的专业知识&#xff0c;广泛的人际关系&#xff0c;培养我成为一个敢于承担责任&#xff0c;对待生活乐观积极&#xff0c;吃苦耐劳的青年。在专业方面我的主攻方向是计…

Ffmpeg6.0版本源码解读第一期!

前言&#xff1a; 大家好&#xff0c;最近一直在直播讲解Ffmpeg6.0版本的源码解析&#xff0c;这里要明白学习源码能给我们带来什么好处&#xff1f;我相信很多小伙伴已经用过Ffmpeg去开发&#xff0c;不知道大家有没有在开发的过程&#xff0c;调用接口的时候&#xff0c;是否…

PowerDesigner面向对象建模-常用UML图

1 PowerDesigner简介 PowerDesigner最初由Xiao-Yun Wang&#xff08;王晓昀&#xff09;在SDP Technologies公司开发完成。PowerDesigner是Sybase的企业建模和设计解决方案&#xff0c;采用模型驱动方法&#xff0c;将业务与IT结合起来&#xff0c;可帮助部署有效的企业体系架…

python熟悉python基础语法,了解html网络结构,了解json格式数据,含有字符串

前言 Python网络爬虫是利用Python编写的程序&#xff0c;通过自动化地访问网页、解析html或json数据&#xff0c;并提取所需信息的技术。下面将详细介绍一些与Python网络爬虫相关的重要知识点。 1、Python基础语法&#xff1a; 变量和数据类型&#xff1a;学习如何声明变量以及…

使用R语言绘制富集条形图,轻松分析基因表达数据

一、引言 富集分析&#xff08;enrichment analysis&#xff09;是一种生物信息学方法&#xff0c;它可以帮助我们识别基因或其他的生物实体在某个特定的类别中过度表示的趋势。通俗来说&#xff0c;富集分析通过将基因分类到特定的集合中&#xff0c;然后根据基因在集合中的分…

万字长文带你深入理解JavaNIO并手动实现多人聊天室

NIO 网络编程 代码已同步至GitCode&#xff1a;https://gitcode.net/ruozhuliufeng/java-project.git Java NIO简介 IO概述 ​ IO的操作方式通常分为几种&#xff1a;同步阻塞BIO、同步非阻塞NIO、异步非阻塞AIO。 ​ &#xff08;1&#xff09;在JDK1.4之前&#xff0c;我们…

哈希表/散列表(HashTable)c++实现

目录 哈希表实现的思想 除留余数法 哈希冲突 第一种方法&#xff1a;探测法实现哈希表 探测法的思想 结点类 插入数据(insert) 冲突因子 数据扩容 哈希值 插入的代码实现以及哈希类 查找数据(find) 删除数据(erase) 第二种方法&#xff1a;拉链法实现哈希表 …

Kotlin~迭代器模式

概念 提供一种遍历集合元素的方法&#xff0c;而不暴露集合内部的实现。 角色介绍 iterator 迭代器接口: 定义访问和遍历集合元素的接口&#xff0c;一般包含next和hasNext方法。concrete iterator 具体迭代器: 实现迭代器接口&#xff0c;迭代器的核心逻辑实现。aggregate …

极致呈现系列之:Echarts热力图的神奇光晕

目录 什么是热力图热力图的特性及应用场景热力图的特性热力图的应用场景 Echarts中热力图的常用属性vue3中创建热力图 什么是热力图 热力图&#xff08;Heatmap&#xff09;是一种基于颜色映射的数据可视化图表&#xff0c;用于展示数据点的密度和分布情况。它使用不同的颜色强…

RT-Thread-10-线程优先级翻转

线程优先级翻转 前面讲到信号量和互斥量&#xff0c;二者有些区别&#xff1a; 信号量&#xff0c;可以在任何线程&#xff08;以及中断&#xff09;释放&#xff0c;用于同步&#xff0c;线程只在获得许可时才可以运行&#xff0c;强调的是运行步骤&#xff1b; 互斥量&#…

科技项目验收测试规范有哪些?

随着科技的不断发展和进步&#xff0c;越来越多的科技项目被投入使用。为了保证这些科技项目的质量&#xff0c;需要进行验收测试。科技项目验收测试是一项非常重要的工作&#xff0c;其结果对项目的质量和功能正常使用有着直接的影响。本文将就科技项 目验收测试规范和第三方软…