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

为什么要加锁

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

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

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

相关文章

如何购买阿里云服务器

作为一家全球领先的云计算服务提供商,阿里云提供了多种云产品和解决方案,包括云服务器、对象服务OSS、数据库、存储、SSL、域名和CDN等。阿里云服务器是一种灵活可扩展的云计算服务,适用于各种规模和类型的企业和个人用户。阿里云以其出色的性…

重启路由器可以解决N多问题?

为什么重启始终是路由器问题的首要解决方案? 在日常的工作学习工作中,不起眼的路由器是一种相对简单的设备,但这仍然是我们谈论的计算机。 这种廉价的塑料外壳装有 CPU、随机存取存储器 (RAM)、只读存储器 (ROM) 和许多其他组件。 该硬件运行预装的软件(或固件)来管理连接…

【多线程】-- 10 线程同步synchronized方法/块

多线程 6 线程同步 同步方法 由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括以下两种用法: ​ synchronized方法和synchronized块 …

会声会影2023序列号免费永久序列号和激活码下载使用(附破解补丁)

前言 会声会影2023破解版免费下载是经过修改的视频剪辑软件,它能够免费为您提供很多功能。会声会影2023免费下载提供超过 1500 种独特的效果,可让您提升自我。会声会影破解版是用于是制作独一无二的视频的最强大、功能最全的软件。 它是一个简单而快速的…

【算法】滑动窗口题单——5.多指针滑动窗口醒醒⭐

文章目录 930. 和相同的二元子数组解法1——前缀和 哈希表解法2——滑动窗口 ⭐ 1248. 统计「优美子数组」1712. 将数组分成三个子数组的方案数⭐⭐⭐2444. 统计定界子数组的数目解法——多指针滑动窗口代码2——简洁写法:一次遍历O(1) 空间🐂⭐ 992. K…

docker部署jupyter

文章目录 1.搜索镜像2.拉取镜像3.创建挂载4.运行容器4.查看容器运行运行状态5.token查看6.访问jupyter 1.搜索镜像 docker search jupyter: 命令用于在 Docker Hub 上搜索名为 “jupyter” 的镜像。搜索结果显示了一个名为 “jupyter/datascience-notebook” 的镜像&#xff0…

掌握大型语言模型(LLM)技术:推理优化

原文链接:Mastering LLM Techniques: Inference Optimization | NVIDIA Technical Blog 大模型相关技术文章已整理到Github仓库,欢迎start! 堆叠Transformer层以创建大型模型可以获得更好的准确性、few-shot学习能力,甚至在各种语言任务中具有…

python 运用pandas 库处理excel 表格数据

文章目录 读取文件查看数据数据选择数据筛选创建新列计算并总结数据分组统计 读取文件 Pandas 是一个强大的数据分析库,它提供了丰富的数据结构和数据分析工具,其中之一是用于读取不同格式文件的 read_* 函数系列。以下是一个简单介绍如何使用 Pandas 读…

HDMI之数据岛

概述 发送端在发送视频信号之前,将多媒体信息通过数据岛传输给接收端。接收端通过数据岛信息获取当前分辨率(VIC),编码信息(RGB/YCR等),色彩空间,位深等等。然后对应将视频信息解码。与此同时,多余的带宽用于传输音频信息等。本文通过具体的包信息(从实验室仪器拍照…

3.5.6 轮询访问介质访问控制

目录 介质访问控制轮询协议令牌传递协议 介质访问控制 信道划分介质访问控制(MAC Multiple Access Control)协议: 基于多路复用技术划分资源网络负载重:共享信道效率高,且公平网络负载轻:共享信道利用率低…

常微分方程组的数值解法(C++)

常微分方程组的数值解法是一种数学方法, 用于求解一组多元的常微分方程(Ordinary Differential Equations, ODEs). 常微分方程组通常描述了多个变量随时间或其他独立变量的演化方式, 这些方程是自然界和工程问题中的常见数学建模工具. 解这些方程组的确切解通常难以找到, 因此需…

WordPress外贸站优化工具,WordPress外贸SEO优化方法

WordPress外贸站是跨国企业拓展市场、提升品牌知名度的理想选择。然而,如何通过SEO优化、原创文章生成以及留心站点优化的事项,成为众多站长关注的焦点。 SEO,即搜索引擎优化,是提高网站在搜索引擎结果中排名的关键。首先&#x…

Linux——基本指令(一)

写在前面: 我们云服务器搭建的Linux系统,使用的镜像版本CentOS 7.6,使用的Xshell远程连接云服务器 前面我们使用超级管理员root账号登录,一般我们使用普通用户登录,那么如何创建新用户呢? 1.创建新用户 &#xff08…

jsp 管理员登录界面与登录验证

验证分两种情况 &#xff0c;成功&#xff0c;进入管理员页&#xff0c;可以看信息和删记录 失败&#xff0c;直接给出登录失败&#xff0c;然后重新登录 login.jsp <% page language"java" contentType"text/html; charsetUTF-8"pageEncoding"UTF…

图片处理OpenCV IMDecode模式说明【生产问题处理】

OpenCV IMDecode模式说明【生产问题处理】 1 前言 今天售后同事反馈说客户使用我们的图片处理&#xff0c;将PNG图片处理为JPG图片之后&#xff0c;变为了白板。 我们图片处理使用的是openCV来进行处理 2 分析 2.1 图片是否损坏&#xff1a;非标准PNG头部 于是&#xff0c;马…

Matter学习笔记(3)——交互模型

一、简介 1.1 交互方式 交互模型层定义了客户端和服务器设备之间可以执行哪些交互。发起交互的节点称为发起者&#xff08;通常为客户端设备&#xff09;&#xff0c;作为交互的接收者的节点称为目标&#xff08;通常为服务器设备&#xff09;。 节点通过以下方式进行交互&a…

音频处理关键知识点

1 引言 现实生活中&#xff0c;我们听到的声音都是时间连续的&#xff0c;我们称为这种信号叫模拟信号。模拟信号需要进行数字化以后才能在计算机中使用。 目前我们在计算机上进行音频播放都需要依赖于音频文件。音频文件的生成过程是将声音信息采样、量化和编码产生的数字信号…

Pandas实战:电商平台用户分析

数据分析 1.行为概况 首先&#xff0c;我们要对用户的行为类型有一定的理解&#xff0c;了解每个行为所代表的含义。 浏览&#xff1a;作为用户与商品接触的第一个行为&#xff0c;它的数量级与其他行为类型相比而言是非常庞大的&#xff0c;因为&#xff1a; 用户购买之前需…

Linux系统配置深度学习环境之cudnn安装

前言 一个针对深度学习应用优化的 GPU 加速库。它提供了高性能、高可靠性的加速算法&#xff0c;旨在加速深度神经网络模型的训练和推理过程。 cuDNN 提供了一系列优化的基本算法和函数&#xff0c;包括卷积、池化、规范化、激活函数等&#xff0c;以及针对深度学习任务的高级功…

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

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