并发编程2:Java 加锁的原理和JVM对锁的优化

news2025/1/13 13:30:21

为什么要加锁

  • 在多进程的环境下,如果一个资源被多个进程共享,那么对资源的使用往往会表现的随机和无序,这显然是不行的。例如多个线程同时对控制台输出,每个线程都输出的是完整的句子但是多个线程同时同时输出,则输出的内容就会被完全打乱,获取不到本来的信息了。
  • 对于这种共享资源,需要进行同步管理,资源在被一个线程占用时,其他线程只能阻塞等待。
  • Java 的同步就是使用的对象锁机制来实现的,要使用资源则先获取资源对应的锁后才能操作。


一、 Synchronized 关键字的作用是给对象加锁

  1. java 中的多线程同步机制通过对象锁来实现,Synchronized 关键字则是实现对对象加锁来实现对共享资源的互斥访问。
  2. synchronized 关键字实现的是独占锁或者称为排它锁,锁在同一时间只能被一个线程持有。
  3. JVM 的同步是基于进入和退出监视器对象(Monitor 也叫管城对象)来实现的,每个对象实例都有一个 Monitor 对象,和 Java 对象一起创建并一起销毁。
  4. Java 编译器,在编译到带有synchronizedg 关键字的代码块后,会插入 monitorenter 和 monitorexit 指令到字节码中,monitorenter 也就是加锁的入口了,线程会为锁对象关联一个 ObjectMonitor 对象。


二、对象基于 ObjectMonitor 加锁的原理


2.1 对象在内存中的布局


2.2 ObjectMonitor 监视器

//结构体如下
ObjectMonitor::ObjectMonitor() {  
    _header       = NULL;  
    _count       = 0;  
    _waiters      = 0,  
    _recursions   = 0;   	 //线程的重入次数
    _object       = NULL;  
    _owner        = NULL;    //标识拥有该monitor的线程
    _WaitSet      = NULL;    //等待线程组成的双向循环链表,_WaitSet是第一个节点
    _WaitSetLock  = 0 ;  
    _Responsible  = NULL ;  
    _succ         = NULL ;  
    _cxq          = NULL ;    //多线程竞争锁进入时的单向链表
    FreeNext      = NULL ;  
    _EntryList    = NULL ;    //_owner从该双向循环链表中唤醒线程结点,_EntryList是第一个节点
    _SpinFreq     = 0 ;  
    _SpinClock    = 0 ;  
    OwnerIsThread = 0 ;  
}  
  1. ObjectMonitor 是 Java 中的一种同步机制,通常被描述为一个对象,和 Java 对象一起创建一同销毁。

  2. 每一个 Java 对象就有一把看不见的锁,称为内部锁或者 Monitor 锁。

  3. ObjectMonitor 象是一个 C++的结构体,用来维护当前持有锁的线程、阻塞等待锁释放的线程链表、调用了 wait 阻塞等待 notify 的线程链表。

  4. 其中有几个关键属性** EntryList、WaitSet、cxq、owner、recursions**

  5. _cxq 竞争列表:单项链表结构,竞争锁失败的线程,会通过 CAS 将包装成 ObjectWaiter 写入到链表头部,同时为了避免 插入和取出元素的竞争,Owner 会从列表尾部取出元素。

  6. EntryList 锁候选者列表:双向链表结构,如果 EntryList 为空 Cxq 不为空,那么线程释放锁的时候,会将 cxq 中的数据移动到 EntryList 中,并制定 EntryList 列表的头结点线程作为 OnDeck 线程。

    1. OnDeck 是可以进行锁竞争的线程,如果线程是 OnDeck 状态,那么可以进行 tryLock 操作,如果失败则重新回到 EntryList 的头部。
    2. 因为 cxq 中的线程可以自旋,所以 OndeckThread 仍然有可能竞争失败。
  7. WaitSet:双向链表结构,保存由于不满足执行条件获取锁后主动释放锁 wait 的线程,在被 notify/notifyAll 后会重新参与锁竞争。

  8. owner:指向持有 ObjectMonitor 对象的线程

  9. recursions:记录当前锁的重入次数

2.3 ObjectMonitor 基本工作机制

  1. 所有期待获得锁的线程,在锁已经被其它线程拥有的时候,这些期待获得锁的线程就进入了对象锁的entry set区域。
  2. 所有曾经获得过锁,但是由于其它必要条件不满足而需要wait的时候,线程就进入了对象锁的wait set区域 。
  3. 在wait set区域的线程获得Notify/notifyAll通知的时候,随机的一个Thread(Notify)或者是全部的Thread(NotifyALL)从对象锁的wait set区域进入了entry set中。
  4. 在当前拥有锁的线程释放掉锁的时候,处于该对象锁的entryset区域的线程都会抢占该锁,但是只能有任意的一个Thread能取得该锁,而其他线程依然在entry set中等待下次来抢占到锁之后再执行。

2.4 执行流程图

2.5 ObjectMonitor::enter() 加锁的过程

  1. 如果当前线程已经是 owner 则加锁直接成功,只是加锁重入次数recursions+1
  2. 如果当前线程没有被加锁 即 owner 为空,则尝试 CAS 竞争加锁
  3. 如果当前线程已经被锁定,则阻塞进入等待队列EntryList 等待释放后再竞争锁,如果 EntryList 超出阈值线程将会阻塞一直到线程数量减少或被其他线程唤醒。

2.6 ObjectMonitor 竞争锁的过程

  1. 加锁的过程就是多个线程尝试 CAS 操作将 ObjectMonitor 的 owner 设置为自身,并增加重入次数。
  2. 如果当前线程加锁失败,未能获取到锁,则线程会启动自适应自旋,会循环尝试加锁。这是为了避免线程阻塞的开销。
  3. 自旋结束仍未获取到锁,则会被包装成 ObjectWaiter 对象,通过 addwaiter 方法加入到 _cxq 竞争队列的头部
  4. 加入 cxq 队列后,线程仍会再次尝试 CAS 加锁操作,失败后就会被 park 挂起。直到被唤醒重新竞争锁。

2.7 ObjectMonitor::wait() 让出锁

  1. 如果线程执行后判断不满足后续运行条件,会选择调用 wait 进入等待状态
  2. 线程会被封装成 ObjectWaiter 对象,最后会被使用 park 方法挂起。
  3. 调用 wait 第一步会将自身加入到 _waitSet 这个双向链表,后续再调用ObjectMonitor::exit() 来释放锁

2.8 ObjectMonitor::exit() 释放锁的过程

  1. 持有锁的线程执行完 加锁的临界区代码后,会使用ObjectMonitor::exit()来释放锁。
  2. 释放锁会将当前的 _owner 设置为空
  3. 会根据策略,选择将 cxq 队列中的线程移动到 EntryList 队列中唤醒 EntryList 的头部节点 或者直接唤醒 cxq 队列的头部节点让其竞争锁。
  4. 锁被成功释放后,会将栈帧中的 MarkWord 替换回原来的对象头中。

2.9 Object::notify 方法 执行过程

  1. 如果 waitSet 为空,则直接结束
  2. 从 waitSet 头部取出线程节点一个 ObjectWaiter 对象,根据策略 QMode 决定,将线程节点放在哪儿可能放在 cxq 队列头部或者 EntryList 的头部或者尾部,或者被直接唤醒开始竞争锁。
  3. 这样下次锁被释放时,它就能重新参与竞争锁了。


三、 Java 对同步机制的优化

在 jdk1.6 之前,对于并发控制就只有synchronized 这种办法,如果一个线程已经获得锁,另一个线程就只能阻塞进入等待,后续的线程调度就只能由操作系统来控制了。操作系统对线程的调度,需要频繁的上下文切换,所以效率很低。
来到 jdk1.6 JVM 对加锁进行了一系列的优化

3.1 锁的升级机制

3.2 32 位 JVM 的 markWord 结构

yuque_diagram.png

3.3 偏向锁机制

  1. JDK 1.6 默认开启偏向锁,但是在 JDk15 之后就是默认关闭了,因为偏向锁给 JVM 增加了巨大的复杂性。
  2. 未加锁时,锁标志位是 01 并且 markword 中包含 HashCode 值的位置
  3. 施加偏向锁后,markword 中会保存锁的线程 id、epoh 时间戳等信息,同时偏向锁标识变为 1
  4. 开启偏向锁后,进行加锁会判断偏向锁的线程 id 是否和 markword 线程 id 一致,一致则说明加锁成功可以执行临界区代码;
  5. 如果不一致则检查是否已偏向某个线程,未偏向则使用 CAS 加锁;未偏向的情况下加锁失败或者存在偏向但不一致,则说明存在竞争。锁会升级成轻量级锁,或者重新偏向。
  6. 偏向锁只有在出现其他线程竞争时,才会释放,线程不会主动释放偏向锁。
  7. 偏向锁在调用 wait 方法时会直接升级成重量级锁,因为 wait 方法是重量级锁独有的。
  8. hashcode 一般会在第一次调用时填入 markword,如果对象已经计算过 hashcode 那么永远无法进入偏向锁状态。如果已经处于偏向锁状态收到计算 Hashcode 的请求,则会膨胀成为重量级锁,对象头指向重量级锁,重量级锁 ObjectMonitor 类中有字段可以记录未加锁状态的 MarkWord

3.4 轻量级锁

如果竞争不激烈,一次获取锁失败就立即进入阻塞状态,那么可能刚进入阻塞状态就立即被唤醒进行加锁。这就会带来上下文的切换,所以轻量级锁获取锁失败时,会进行一定次数或时间的自旋尝试反复获取锁。如果失败则再进入阻塞。

  • 当发生多个线程竞争时,偏向锁会变为轻量级锁,锁标志位为00
  • 获得锁的线程会先将偏向锁撤销(在安全点),并在栈桢中创建锁记录LockRecord,对象的MarkWord被复制到刚创建的LockRecord,然后CAS尝试将记录LockRecord的owner指向锁对象,再将锁对象的MarkWord指向锁,加锁成功
  • 如果CAS加锁失败,线程会自旋一定次数加锁,再失败则升级为重量级锁

3.5 重量级锁(Synchronize 基于监视器实现的锁机制)

  • 竞争线程激烈,锁则继续膨胀,变为重量级锁,也是互斥锁,锁标志位为10,MarkWord其余内容被替换为一个指向对象锁Monitor的指针

3.6 锁粗化

多次加锁操作在JVM内部也是种消耗,如果多个加锁可以合并为一个锁,就可减少不必要的开销。例如一个方法中将代码分成两个加锁的代码块并且是同一个锁对象,则可以合并为一次加锁过程。

3.7 锁消除

如果涉及变量只是一个线程的栈变量,不是共享变量,编译器会尝试消除锁

3.8 分段锁

分段锁不是真正的某种锁,而是使用锁的一种方式;主要就是将大对象拆成小对象,对大对象的加锁变成了对小对象的加锁,避免锁住整个对象。CurrentHashMap 就是这种操作

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

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

相关文章

sd_webui的实用插件,prompt/lama/human matting/...,持续开源更新!!

热烈欢迎大家在git上star!!!冲鸭!!! 1.prompt优化插件 GitHub - leeguandong/sd_webui_beautifulprompt: beautifulprompt extension performs stable diffusion automatic prompt engineering on a bro…

博途PLC数组指针应用(SCL)

CODESYS数组类型变量使用介绍 https://rxxw-control.blog.csdn.net/article/details/131375218https://rxxw-control.blog.csdn.net/article/details/131375218 博途PLC数组类型变量使用介绍还可以查看下面文章博客: https://rxxw-control.blog.csdn.net/article/details/1…

通义灵码简单使用例子

首先我们需要了解到通义灵码的能力: 行/函数级实时续写: 当我们在 idea进行代码编写时(确认开启了自动云端生成的模式),通义灵码会根据当前代码文件及相关代码文件的上下文,自动为你生成代码建议。你可以不用,也可以t…

Android把宽高均小于给定值的Bitmap放大到给定值,Kotlin

Android把宽高均小于给定值的Bitmap放大到给定值,Kotlin 假设拉伸放大到SIZE2048 fun scaleSize(image: Bitmap): Bitmap {val w image.widthval h image.heightvar newW: Intvar newH: Intif (w > h) {newW SIZEnewH (SIZE / w.toFloat()) * h} else {newW …

计算机网络TCP篇③问答篇

目录 一、如何理解 TCP 是面向字节流协议 先来说说为什么 UDP 是面向报文的协议? 如果收到了两个 UDP 报文,操作系统是如何区分开的呢? 再说说为什么 TCP 是面向字节流的协议? 二、如何解决粘包问题? ①、固定消…

23.12.3日总结

饿了么项目进度 新建菜品的添加属性: 适应不同尺寸的媒体查询: 菜品详细页面: 项目上遇到的问题 媒体查询遇到的问题: 关于媒体查询不能生效的原因-CSDN博客 答辩总结 js中声明变量的关键字(const,let…

前缀和例题:子矩阵的和AcWing796

//前缀和模板提,在读入数据的时候就可以先算好前缀和的大小 //计算前缀的时候用:g[i][j] g[i][j-1] g[i-1][j] - g[i-1][j-1] Integer.parseInt(init[j-1]); //计算结果的时候用:g[x2][y2] - g[x1 - 1][y2]- g[x2][y1-1] g[x1 -1][y1 - 1] "\n" //一些重复加的地…

YOLOv8 区域计数 | 入侵检测 | 人员闯入

大家好,昨天的 YOLOv8 新增加了一个功能,区域计数,用这个功能我们能实现很多的任务, 比如入侵检测,流量统计,人员闯入等,使用方式也非常的方便,但是一定要使用最新版的 YOLOv8 代码(2023/12/03更新的代码)。 低版本是不具备这个功能的,上面是演示效果。 使用非常的方…

InsCode实践分享

在当今信息爆炸的时代,如何从海量信息中脱颖而出,获取更多的关注和认可,成为了许多人的共同追求。作为知乎平台上的优质用户,我愿意分享一些自己的经验和技巧,帮助大家更好地运用InsCode,实现个人成长和进步…

细说CountDownLatch

CountDownLatch 概念 CountDownLatch可以使一个获多个线程等待其他线程各自执行完毕后再执行。 CountDownLatch 定义了一个计数器,和一个阻塞队列, 当计数器的值递减为0之前,阻塞队列里面的线程处于挂起状态,当计数器递减到0时…

Linux下activemq的安装与安装成功确认

一、下载 apache-activemq-5.14.0-bin.tar.gz 二、安装 将压缩包拷入linux内,进行解压tar -zxvf apache-activemq-5.14.0-bin.tar.gz,与redis、nginx不同的是,active解压不需要安装就可以直接启动! 启动命令:./bin…

某60区块链安全之Create2实战二学习记录

区块链安全 文章目录 区块链安全Create2实战二实验目的实验环境实验工具实验原理实验内容Create2实战二 实验步骤Create2实战二 实验目的 学会使用python3的web3模块 学会分析以太坊智能合约中的伪随机数问题 学会利用Create2可在同一地址部署不同合约特性解决伪随机数问题 找…

❀My学习Linux命令小记录(6)❀

目录 ❀My学习Linux命令小记录(6)❀ 26.ps指令 27.grep指令 28.awk指令 29.sed指令 30.wc指令 ❀My学习Linux命令小记录(6)❀ 26.ps指令 功能说明:报告当前系统的进程状态。 (ps.ps命令 用于报告当前系统的进…

免费数据采集软件,多种数据采集方式

数据无疑是企业决策的关键驱动力。要充分利用数据,就需要进行数据收集,而数据采集的方式多种多样。 数据采集方式的丰富多彩 数据采集并非一蹴而就的简单任务,而是一个多层次、多步骤的过程。在这个过程中,我们有着多种数据采集…

数据结构实验任务六 :基于 Dijsktra 算法的最短路径求解

本次代码为实验六:基于 Dijsktra 算法的最短路径求解实现。本实验的重点在于对于Dijsktra算法的理解。有关Dijsktra的资料可以参考有关博文: 图论:Dijkstra算法——最详细的分析,图文并茂,一次看懂!-CSDN博客 以下附上…

[LeetCode周赛复盘] 第 374 场周赛20231203

[LeetCode周赛复盘] 第 374 场周赛20231203 一、本周周赛总结100144. 找出峰值1. 题目描述2. 思路分析3. 代码实现 100153. 需要添加的硬币的最小数量1. 题目描述2. 思路分析3. 代码实现 100145. 统计完全子字符串1. 题目描述2. 思路分析3. 代码实现 100146. 统计感冒序列的数…

USART的PAL库编程

USART驱动的工作原理 总结一下我们之前使用中断的方式来进行数据的发送和接收 如果收到数据数据在RDR寄存器中 RXNE标志位就从0到1触发中断 进入中断服务函数 把数据缓存在队列中 然后在到进程函数中断接收数据函数中进行出队处理 发送数据就是把中断关闭(标志位TXE…

《Junit单元测试》

目录 SpringBoot2.2.0版本之前的单元测试模式 SpringBoot2.2.0版本之后的单元测试模式 SpringBoot2.4以上版本移除了默认对Vintage的依赖 SpringBoot2.2.0版本之前的单元测试模式 SpringBooot 2.2.0 版本开始引入Junit5作为单元测试默认库,之前的版本是使用Junit…

线程...

文章目录 1.Linux中线程该如何理解2.重新定义线程 和 进程3.重谈地址空间 --- 第四讲4.Linux线程周边的概念 线程:是进程内的一个执行分支。线程的执行粒度,要比进程要细 很多教材喜欢这么说,这只是一个线程的特征之一,来解释线程。 1.Linux中线程该如何…

力扣 --- 最后一个单词的长度

题目描述: 给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。 单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。 示例 1: 输入:s "Hello World&…