【JMM】并发编程Bug的源头——可见性/有序性/原子性问题

news2024/11/25 13:36:29

本文目录( ̄∇ ̄)/

可见性问题

有序性问题

为什么会进行指令重排序/乱序执行?

乱序存在的条件

this对象的溢出

原子性问题

如何保证原子性?

synchronized 原理简介

加锁的方式

那么问题来了,JVM是如何知道当前对象有没有加锁呢🤔?

对象的内存布局

锁升级过程

锁粗化

锁消除

逃逸分析(JDK1.7起默认开启)

锁重入

其他相关问题

附📎 volatile 🆚 synchronized​​​​​​​


可见性问题

可见性指的是一个线程对共享变量的修改对其他线程是可见的

可见性问题是指当多个线程并发访问共享变量时,一个线程对共享变量的修改对其他线程可能不可见的情况,而可见性问题是由于线程之间的独立性本地缓存的存在而引起的

在现代计算机体系结构中,每个线程都有自己的本地缓存,这样可以提高读写操作的性能,当一个线程修改了共享变量的值时,它可能会将修改后的值暂存在自己的本地缓存中,而不是立即写回到主存中,其他线程在读取该共享变量时,可能会直接从自己的本地缓存中读取,而不是从主存中获取最新的值,这样就可能导致线程之间对共享变量的修改不可见。

可见性问题可能导致以下情况发生:

  1. 脏读(Dirty Read):一个线程读取到了另一个线程未提交的修改结果

  2. 重复读(Lost Update):多个线程同时读取同一个共享变量,并进行修改后写回,但由于可见性问题,某些线程的修改结果被覆盖,导致部分修改丢失

  3. 无效读(Stale Read):一个线程读取到了另一个线程已经修改的结果,但该结果已经过时

总所周知,volatile 关键字可以解决能够及时可见的问题,使得修改过的数据能立刻被看到,

其实现不同线程之间的可见性方法有很多,可以通过内存屏障,或者通过上下文切换,不管表面形式如何,其底层原理就是通过某些方式/语句触发内存缓存同步刷新,即需要满足两点

  • 对线程本地变量的修改可以立刻刷新回主内存

  • 同时使得其他线程中该变量的缓存失效

对有关 “volatile 关键字解决可见性的原理是什么?”“以及还有什么方式也可以解决线程间的可见性问题?”问题感兴趣的客官可以移步这篇

【JMM】保证线程间的可见性,还只知道volatile?_AQin1012的博客-CSDN博客

有序性问题

有序性问题是指在多线程环境下,多个线程可以同时执行,它们之间的操作和指令可能会交错执行,从而导致程序执行的顺序可能与期望的顺序不一致(即执行顺序的不确定性),进一步导致结果不正确或不符合预期

为什么会进行指令重排序/乱序执行?

为了提高效率

乱序存在的条件

  • as-if-serial(看着像是序列化)
  • 不影响单线程的最终一致性

this对象的溢出

不要构造方法中启动线程,可能会造成对象溢出

原子性问题

原子性指的是一个操作是不可分割的,要么全部执行成功,要么全部不执行

原子性问题是指在多线程环境下,一个或多个操作在执行过程中不被中断的特性。原子性问题出现的原因是多线程环境中,多个线程对共享资源进行读取、写入、修改等操作时,可能会发生竞态条件(Race Condition),导致部分操作被其他线程中断或覆盖,从而产生错误的结果。

原子性问题可能导致以下情况发生:

  1. 丢失更新(Lost Update):多个线程同时读取同一个共享变量,并进行修改后写回,但由于原子性问题,某些线程的修改结果被覆盖,导致部分修改丢失
  2. 覆盖写(Overwriting):多个线程同时写入同一个共享变量,其中一个线程的写入操作会覆盖其他线程已经写入的结果,导致部分数据丢失或错误
  3. ABA问题:在使用CAS(Compare and Swap)等原子操作时,一个线程读取到一个共享变量的值,然后其他线程对该变量进行了一系列修改,最后又回到了原始值,使得读取线程无法察觉到这个过程,导致出现不一致的结果

主存和工作内存交互的8大原子性操作(JVM级别)

  • 数据安全

    • lock(锁定)
    • unlock(解锁)
  • 数据交互

    • read(读取)
    • load(加载)
    • use(使用)
    • assign(赋值)
    • store(存储)
    • write(写入)

可以按下图捋捋流程

如何保证原子性?

  • 上锁🔒(如synchronized关键字、Lock接口)来确保同一时间只有一个线程对共享资源进行操作,从而保证原子性

  • 使用原子变量(如AtomicInteger、AtomicLong、AtomicReference等)进行操作,这些变量提供了一些原子操作方法,可以确保读取、写入、比较等操作的原子性(一些线程安全的集合对象内部也是使用了原子变量)

上锁的本质是使并发操作序列化,因此会降低效率,接下来我们主要介绍下synchronized关键字(synchronized保证了可见性,但不能保证有序性)

synchronized 原理简介

synchronized内置锁是一种对象锁,锁的是对象,而非引用,作用粒度是对象,可以用来实现对临界资源的同步互斥访问,并且可重入

在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为内部对象Monitor(监视器锁)是依赖于底层的操作系统的 Mutex Lock 实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要切换到操作系统的内核态来完成,这个状态切换导致早期的 synchronized 效率较低。

JDK1.6 JVM 层面对synchronized 实现了较大优化,对锁的实现引入如自旋锁、偏向锁、轻量级锁等技术来减少锁操作的开销

加锁的方式

  • 同步实例方法 -> 锁的当前实例对象(this)
  • 同步类方法 -> 锁的当前类对象
  • 同步代码块 -> 锁的是括号里面的对象

那么问题来了,JVM是如何知道当前对象有没有加锁呢🤔?

这就需要提一下JVM中对象的内存布局了

对象的内存布局

Java的锁是加在对象上的,来咱们用图说话( ̄∇ ̄)/

 锁状态有:无锁态、轻量级锁、偏向锁、重量级锁,考虑到空间利用效率,Mark Word的部分会根据锁状态的不同而有所区别(详见下图)

锁状态

是否禁用偏向

锁标志位

无锁态(new)

对象的HashCode

age(分代年龄)

0

01

偏向锁

Epoch

ThreadID

(当前获取偏向锁的线程ID)

age

(分代年龄)

1

01

轻量级锁

自旋锁

指针(指向栈中锁记录LockRecord)

00

重量级锁

指针(指向重量级锁(互斥量))

10

GC标记

11

可以看到,在对象的header的Mark Word中(synchronized优化的过程和Mark Word息息相关)记录了锁的状态,从无锁态到偏向锁、再到轻量级锁、最后重量级锁,这其实就是JVM优化了synchronized后的锁升级过程,接下来我们详细的介绍下这个过程。

锁升级过程

先来看一张图

 在JVM刚启动时,会有一个几秒的延迟(默认4秒,可以使用-XX:BiasedLockingStartupDelay=0来取消延时),在这几秒钟为无锁态,随后会进入到匿名偏向的状态中,此时当有一个线程来加锁,匿名偏向就会升级成偏向锁,此时如果再来其他线程就会升级成轻量级锁,当达到特定的线程自旋次数,或者自旋线程数即“竞争激烈”的就会升级成重量级锁。

锁粗化

多个锁结构优化成一个锁机构(效果一样)

锁消除

是虚拟机的一种锁的优化,虚拟机在JIT编译时(简单理解就是当某段代码第一次进行编译,即及时编译),通过对运行上下文的扫描,去除不可能出现资源竞争的锁。通过这种方式消除没有必要的锁,可以节省时间。锁消除的依据是逃逸分析的数据支持。

逃逸分析(JDK1.7起默认开启)

Java中的逃逸分析是指编译器分析对象的作用域,确定它是否可以在方法调用之后被其他代码引用

如果对象没有逃逸,则可以将其存储在栈上而不是堆上,这可以提高程序的性能。逃逸分析可以在运行时对程序进行优化,因为它可以帮助编译器更好地了解代码的执行情况并进行针对性的优化。通过逃逸分析,编译器可以推断出哪些对象可以安全地存储在栈上,以便在程序运行时减少堆内存的使用,从而提高程序的性能和效率。

使用-XX:+DoEscapeAnalysis开启逃逸分析,编译器可以对代码作如下优化

  • 栈上分配:对于一些局部对象,编译器可以在栈上分配空间,避免了在堆上进行内存分配和回收的开销

    • 因此并非所有的对象都会在堆内存分配空间
  • 同步省略:如果一个对象被发现只能从一个线程被访问到,那么这个对象的操作可以考虑不同步
  • 分离对象或标量替换

    • 直接把对象中的标量创建在栈中,可以有效减少GC

Java中的8大基本数据类型称为标量对象称为聚合量(由基本数据类型组成)

锁重入

  • 偏向锁的锁记录在线程栈中,每重入一次,压入栈中一个LockRecord对象,解锁的时候一次向外弹,LockRecord对象弹完,锁解开;
  • 轻量级锁与偏向锁类似;
  • 重量级锁会记录在ObjectMonitor的一个字段上

其他相关问题

JVM启动时为什么不直接设置偏向锁,而是进行了延时操作?

因为JVM 虚拟机有很多自己默认启动的线程,里面有很多sync代码,启动时这些代码就会进行竞争,直接使用偏向锁就会不断的进行锁撤销和锁升级(线程竞争会导致撤销偏向锁,升级轻量级锁),影响效率。

线程如何获取到锁?

线程在自己的线程栈中生成LockRecord,用CAS操作将MarkWord中的特定位置设置为指向自己这个线程LockRecord的指针,设置成功者就获取到了锁🔒

偏向锁什么时候升级为轻量级锁 ?

  • 当偏向锁启动时,只要有一个对象来抢,偏向锁就会升级成轻量级锁
  • 当偏向锁为启动时,会直接升级成轻量级锁
  • 调用对象的hashCode也会导致偏向锁升级成轻量级锁

    • 轻量级锁的hashCode记录在线程栈中的LockRecord的MarkWord中
    • 重量级锁的hashCode记录在Monitor中
    • Monitor可以理解为一个同步工具/同步机制(由ObjectMonitor实现的),通常被描述为一个对象,与一切皆对象一样,所有的Java对象是天生的Monitor,即每一个对象都有成为Monitor的潜质(这也是Java v中任何对象都可以作为锁的原因)。在Java的设计中,每个对象都天生带了一把看不见的锁,称为内部锁或者Monitor锁,也就是常说的synchronized的对象锁。当MarkWord中锁标识位为10的对象,其指针指向的是Monitor对象的起始地址。
    • Monitor监视器可以确保监视器上的数据在同一时刻只有一个线程在访问

值得注意的是:偏向锁调用wait()后会直接升级成重量级锁

自旋锁什么时候升级为重量级锁 ?

根据特定的线程自旋次数,或者自旋线程数来作为“竞争激烈与否”的标准

JDK1.6以后采用自适应自旋(Adaptive Self Spinning),由JVM自己控制(无需人工调整)

升级重量级锁需要向操作系统申请资源,用户态切换至内核态(80中断 int 0x80),线程挂起,进入等待队列,等待操作系统的调度,然后再映射回用户空间。

为什么有自旋锁了还需要重量级锁?

自旋是占用CPU资源的,如果锁的时间长或者线程多会非常消耗CPU资源,而将未获取到锁的线程放入重量级锁中的队列中则不会消耗CPU资源

如何解决CAS的ABA问题?

ABA问题是指其他线程修改数次后最后的值与原值相同,使得我们无法仅通过对变量值的比较来判断该变量是否被修改过

对于这种ABA问题,可以通过加版本号进行解决(只要修改版本号就会改变)

如何保证CAS的原子性?

CAS相关的方法都使用的是Atomic类中的变量,Atomic类的底层是通过调用Unsafe类的方法来实现的,而Unsafe中的方法有调用了C++编写的本地方法,到汇编层面,会发现汇编指令中支持一条原语:lock cmpxchg,即可以理解为CAS修改变量名的最终实现是cmpxchg指令,但是cmpxchg指令并不是原子的(可以通过查📖汇编指令的操作手册来确定某条指令是否是原子的),所以前面要加lock,即在执行cmpxchg时,锁住总线、或者该数据所在的缓存行(lock是总线锁还是缓存锁视情况而定),等执行完了,别的线程才能访问

所以,底层还是加锁,被锁住的部分称为“临界区(critical section)”,临界区执行时间长,语句多,称为锁的粒度比较粗,反之,比较细。

附📎 volatile 🆚 synchronized

由上面的情景分析我们可以分别概括下volatile和synchronized的主要特性

  • volatile可以保证可见性有序性(禁止指令重排序),但是不能保证原子性;volatile在修饰引用类型的数据只能保证引用本身的可见性,而不能保证其内部字段的可见性

  • volatile底层是通过lock前缀指令+缓存一致性协议实现的可见性;通过内存屏障(🈲️止指令重排序)实现的有序性

  • synchronized可以保证可见性原子性,但是不能保证有序性

  • synchronized底层是lock语句

  • synchronized内部会根据实际情况进行自适应升级的锁升级

  • synchronized是可重入锁,可重入锁的重入次数必须记录,因为要解锁几次必须要对应

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

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

相关文章

2022年中国标准创新贡献奖获奖名单公示,海尔再添两项标准创新奖

01 2022年中国标准创新贡献奖 获奖名单公示 海尔再添两项标准创新奖 近日,2022年中国标准创新贡献奖获奖名单公示。其中,海尔GB/T 28219—2018《智能家用电器通用技术要求》、T/CAS 311.1—2018《电器电子产品绿色供应链管理第1部分:通则》…

前端实现拖拽效果改变元素顺序

文章目录 前言一、实现效果二、拖拽API1.代码2.遇见问题 总结 前言 在一次工作中,前端要实现通过鼠标实现拖拽改变顺序的功能,之前没有接触过拖拽这一块所以刚开始一筹莫展,幸运的是在查阅学习中实现了前端拖拽功能。 一、实现效果 二、拖拽…

月薪从10k到30k,一个普通测试工程师的3年涨薪之路...

“要涨薪,先跳槽”各个行业都存在这一共识,但是任何行业也都没有像程序员这样更为适用且好用的了。 前不久,就有网友分享了自己作为一个普通的自动化测试工程师的三年真实涨薪经历。但看看这个三年涨薪之路,好像并不普通啊&#…

2022年深圳杯数学建模D题复杂水平井三维轨道设计解题全过程文档及程序

2022年深圳杯数学建模 D题 复杂水平井三维轨道设计 原题再现: 在油气田开采过程中,井眼轨迹直接影响着整个钻井整体效率。对于复杂水平井,较差的井眼轨迹很可能会造成卡钻或施加钻压困难等重大事故的发生。因而,在施工之前分析影…

python爬虫-获取某某在线翻译的查询结果,爬取json文件并解析

文章目录 从基础步骤下手正确获取response数据关于url获取方式关于post方法的参数关于payload参数填入运行效果解析json数据到文件中完整代码运行结果 从基础步骤下手 # 指定url # 发出请求,get或post # 获取响应 # 把目标文件转存为字符串形式 # 持久性保存正确获…

从零开始的机械臂yolov5抓取gazebo仿真(六)

项目构造简述 前段时间博主装20.04系统不小心把efi启动给删了,导致18.04系统崩了,所以只能简单讲一下这个项目的设计思路以及以grasp.py代码为例进行简单解析。 yolov5_ros功能包 首先,说一下yolov5_ros功能包,该功能包的作用就…

使用 CameraX 在 Jetpack Compose 中构建相机 Android 应用程序

使用 CameraX 在 Jetpack Compose 中构建相机 Android 应用程序 CameraX 是一个 Jetpack 库,旨在帮助简化相机应用程序的开发。 [camerax官方文档] https://developer.android.com/training/camerax CameraX的几个用例: Image CaptureVideo CapturePrev…

【多线程】什么是线程死锁?形成条件是什么?如何避免?

文章目录 一、什么是线程死锁二、线程死锁三、形成死锁的四个必要条件是什么四、如何避免线程死锁 一、什么是线程死锁 死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若…

Unity 天空盒

在 Unity 中,天空盒是使用天空盒着色器的一种材质。 创建天空盒材质 1.从菜单栏中,单击 Assets > Create > Material。 2.在 Shader 下拉选单中,单击 Skybox,然后单击要使用的天空盒着色器。 有Skybox/6 Sided、Skybox/…

人民大学与加拿大女王金融硕士项目——在现在憧憬美好的未来

未来是一个虚无缥缈的词汇,抓不住也看不到。未来里有着我们无限的希望,也有着美好的憧憬。未来究竟是怎样的呢,有人说现在的样子里藏着未来的模样。在职的你有没有为未来编织一副美丽的画卷呢?未来很远,远到只能靠想象…

MySQL小记——约束、多表查询

目录 约束 常见约束 主键约束 非空约束 唯一约束 自增长约束 非负约束 外键约束之一对多 外键约束之多对多 多表查询 内连接 外连接 左外连接 右外连接 子查询 自查询 case when语句 约束 在MySQL中,约束是对字段规则的一种限制。 常见约束 1.主…

Linux安装并使用seatunnel2.3.1

SeaTunnel是一个非常易用的超高性能分布式数据集成平台,支持海量数据的实时同步。 下载安装包 设置版本 export version"2.3.1" 通过命令下载 wget "https://archive.apache.org/dist/incubator/seatunnel/${version}/apache-seatunnel-incubat…

Linux/Ubuntu系统运行Python+Yolov5物体识别

程序示例精选 Linux/Ubuntu系统运行PythonYolov5物体识别 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<Linux/Ubuntu系统运行PythonYolov5物体识别>>编写代码&#xff0c;代码…

汽车最强大脑ECU和单片机是什么关系

摘要&#xff1a; 有效解决线路信息传递所带来的复杂化问题 ECU的定义 ECU原来指的是engine control unit&#xff0c;即发动机控制单元&#xff0c;特指电喷发动机的电子控制系统。但是随着汽车电子的迅速发展&#xff0c;ECU的定义也发生了巨大的变化&#xff0c;变成了elec…

31岁才转行程序员,目前34了,我来说说我的经历和一些感受吧...

最近刷知乎&#xff0c;发现有很多朋友有年龄焦虑了&#xff0c;比如&#xff1a;“我今年28了转行来不来得及”&#xff0c;“我今年30了还能转软件测试吗&#xff1f;”......这种问题在知乎上有很多&#xff0c;仿佛大家都觉得年纪大了&#xff0c;很多事情都来不及了&#…

tps和qps的区别和理解

QPS&#xff08;TPS&#xff09; 并发数/平均响应时间 或者 并发数 QPS*平均响应时间 TPS Transactions Per Second&#xff08;每秒传输的事物处理个数&#xff09;&#xff0c;即服务器每秒处理的事务数。TPS包括一条消息入和一条消息出&#xff0c;加上一次用户数据库访…

html使用elementui案例

<!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><title>Title</title><!--引入 element-ui 的样式&#xff0c;--><link rel"stylesheet" href"static/css/index.css">…

轻松将Win10系统备份到U盘的2种方法!

问题&#xff1a;我能将Win10系统备份到U盘吗&#xff1f; ​“我想将Win10系统备份到U盘&#xff0c;然后通过增量或差异备份定期备份。我使用了系统自带的工具进行备份&#xff0c;但它无法识别这个U盘。有没有好用的方法可以轻松的将电脑系统备份到u盘/移动硬盘&#xf…

Ubuntu系统下Nginx安装

一、使用apt安装nginx 0-如果本机安装了nginx&#xff0c;就进行卸载&#xff1a; apt-get --purge autoremove nginx 检查本机是否还有nginx程序在后台运行&#xff0c;如果有直接kill掉。 ps -ef | grep nginx 1-默认版本安装 apt-get update apt-get install nginx 2…

【备战秋招】每日一题:3月18日美团春招第四题:题面+题目思路 + C++/python/js/Go/java带注释

2023大厂笔试模拟练习网站&#xff08;含题解&#xff09; www.codefun2000.com 最近我们一直在将收集到的各种大厂笔试的解题思路还原成题目并制作数据&#xff0c;挂载到我们的OJ上&#xff0c;供大家学习交流&#xff0c;体会笔试难度。现已录入200道互联网大厂模拟练习题&…