Java 对象头、Mark Word、monitor与synchronized关联关系以及synchronized锁优化

1. 对象在内存中的布局分为三块区域:

(1)对象头(Mark Word、元数据指针和数组长度)

对象头:在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit,Java对象头一般占有2个机器码,即64bit,但是 如果对象是数组类型,则需要3个机器码,即96bit,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

Mark Word(32bit)存储代表该对象运行时的一些信息,哈希码、GC分代年龄、锁标志位、偏向线程ID、偏向时间戳等信息。 这些信息都是与对象自身定义无关的数据,所以 Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间 Mark Word 里存储的数据会随着锁标志位的变化而变化。

解读:

(1)前面30bit位可能表示的意思不一样,但是最后2个bit表示的都是锁模式。

(2)当偏向锁标志是0锁标志位是01,也就是最后3位是001的时候,表示无锁模式Mark Word记录的数据就是对象的hashcode 和 GC的年龄。当有第一个线程请求加锁的时候会升级为偏向锁;

(3) 当偏向锁标志是1锁标志是01,也就是最后三位是101的时候,处于偏向锁模式Mark Word这个时候记录的数据就是获取偏向锁的线程IDEpoch对象GC年龄:当有第二个线程请求加锁的时候会升级为轻量级锁;

(4)当锁标志位是00的时候,表示处于轻量级锁模式。会把锁记录放在加锁的线程的虚拟机栈空间中,所以这种情况下锁记录在哪个线程虚拟机栈中,就表示所在线程就获取到了锁Mark Word记录的数据就是就指向那个锁记录地址,这个锁记录地址在哪个线程中,就表示哪个线程获取到了轻量级锁

(5)当锁标志位是10的时候,表示处于重量级锁模式,这个时候就说明竞争激烈了,处于重量级锁模式了,此时使用重量级加锁不是Mark Word的职责范围了是monitor的职责Mark Word 记录的数据就是monitor的地址有加锁的需求直接根据这个地址找到monitor,找monitor加锁。

元数据指针(Klass Point):它主要指向类的数据,也就是指向方法区中的位置,通过这个指针,我们就可以知道该实例属于哪个类,长度通常为32bit。

数组长度(Array Length): 如果是数组,对象头中还有一块用于存放数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。只有数组对象才有,在32位或者64位JVM中,长度都是32bit。

(2)实例数据

实例数据:存放类的属性数据信息,包括父类的属性信息。

(3)对齐填充(非必须)

对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。

2. 通过以上理解可以清楚对象头、Mark Word 和 monitor之间的关系

当Mark Word中最后两位的锁标志位是10的时候,Mark Word的前面是monitor监视器的地址,我现在就给你画出来对象头、Mark Word 和 monitor之间的关系图:

3. synchronized是如何通过monitor加锁的?

3.1 monitor概念

monitor叫做对象监视器、也叫作监视器锁,JVM规定了每一个java对象都有一个monitor对象与之对应,这monitor是JVM帮我们创建的,在底层使用C++实现的

3.2 monitor属性

其实monitor在底层也是某个类的对象,那个类就是ObjectMonitor,它拥有的属性字段如下:

ObjectMonitor() {
        _header = NULL;
        _count = 0;  
// 非常重要,表示锁计数器,_count = 0表示还没人加锁,_count > 0 表示已经加锁,加锁的次数,可重入锁的原理在此,再次执行monitorenter进入后就加1,释放时候执行monitorexit指令前就减1。
        _waiters = 0,
        _recursions = 0;
        _object = NULL;
        _owner = NULL;
 // 非常重要,指向加锁成功的线程,_owner = null 时候表示没人加锁,比如线程A获取锁成功了,则 _owner = 线程A。
        _WaitSet = NULL; // wait线程的集合,在synchorized代码块中调用wait()方法的线程会释放锁,被加入到此集合中沉睡,然后线程就会被挂起,等待别人调用notify叫醒它。
        _WaitSetLock = 0 ;
        _Responsible = NULL ;
        _succ = NULL ;
        //多线程竞争锁进入时的单向链表
        _cxq = NULL ;
        FreeNext = NULL ;
        //_owner从该双向循环链表中唤醒线程节点,_EntryList是第一个节点
        _EntryList = NULL ;
 // 非常重要,等待队列,加锁失败的线程会block住,被加入到这个等待队列中,等待再次争抢锁
        _SpinFreq = 0 ; // 获取锁之前的自旋的次数,

JDK1.6之后对synchronized进行优化;原先JDK1.6以前,只要线程获取锁失败,线程立马被挂起,线程醒来的时候再去竞争锁,这样会导致频繁的上下文切换,性能太差了。

JDK1.6后优化了这个问题,就是线程获取锁失败之后,不会被立马挂起,而是每个一段时间都会重试去争抢一次,这个 _spinFreq就是最大的重试次数,也就是自旋的次数,如果超过了这个次数抢不到,那线程只能沉睡了

        _SpinClock = 0 ; // 获取之前每次锁自旋的时间,上面说获取锁失败每隔一段时间都会重试一次,这个属性就是自旋间隔的时间周期,比如50ms,那么就是每隔50ms就尝试一次获取锁。
        OwnerIsThread = 0 ;
        _previous_owner_tid = 0;
}

3.3 monitor如何通过这些属性加锁

(1)首先呢,没有线程对monitor进行加锁的时候是这样的:_count = 0 表示加锁次数是0,也就是没线程加锁; _owner 指向null,也就是没线程加锁。

(2)此时线程A、线程B来竞争加锁了,都请求将_count 修改为1,此修改具有原子性,同一时间只有一个线程可以修改成功,此时线程A竞争到锁,将 _count 修改为1,表示加锁次数为1,将_owner = 线程A,也就是指向自己,表示线程A获取到了锁。同理可得:释放锁的时候将_count 设置为 0 , 将 _owner 设置为 null 。

(3)在线程A持有锁的时候,monitor里面记录的 _spinFreq 、spinclock 信息告诉线程B,你可以每隔50ms来尝试加锁一次,总共可以尝试10次。

(4)如果线程B10次尝试加锁期间,获取锁成功了,那线程B将 _count 设置为 1, _owner 指向自己表示自己获取锁成功了。

(5)如果10次尝试获取锁此时都用完了,那线程B只能放到等待队列_EntryList里面先睡觉去了,也就是线程B被挂起了。

3.4 线程获取锁失败后的自旋操作好处

这个其实跟jvm获取monitor锁的优化有关。

(1)首先线程挂起之后唤醒的代价很大,底层涉及到上下文切换,用户态和内核态的切换打个比方可能最少耗时3000ms这样,这只是打个比方。

(2)线程A获取了锁,这个时候线程B获取失败。按照上面自旋的数据 _spinclock = 50ms(每次自旋50ms), _spinFreq = 10(最多10次自旋)。

(3)假如线程A使用的时间很短,比如只使用150ms的时间;那么线程B自旋3次后就能获取到锁了,也就花费了150ms左右的时间,相比于挂起之后唤醒最少花费3000ms的时间,大大减少了等待时间,这也就提高了性能了。

(4)如果不设置自旋的次数限制,而是让它一直自旋。假如线程A这哥们耗时特别的久,比如它可能在里面搞一下磁盘IO或者网络的操作,花了5000ms!!。

线程B可不能在那一直自旋着等着它吧,毕竟自旋可是一直使用CPU不释放CPU资源的,CPU这时也在等着不能干别的事,这可是浪费资源啊,所以啊自旋次数也是要有限制的,不能一直等着,否则CPU的利用率大大被降低了。

所以在10次自旋之后,也就是500ms之后,还获取失败,那就把自己挂起,释放CPU资源咯

3.5 monitor的wait和notify

说起monitor里面的waitset,上面讲的就是一个集合

当线程获取锁之后,才能调用wait()方法,然后此时释放锁,将_count恢复为0,将_owner指向 null,然后将自己加入到waitset集合中等待别人调用notify或者notifyAll将其中waitset的线程唤醒

3.6 notify和notifyAll区别?

简单说就是notify就是从waitset随机挑一个线程来唤醒,只唤醒一个。notifyAll这方法就是将waitset中所有等着的线程全部唤醒了。

3.7 wait() 和 Thread.sleep()的区别

wait()会释放锁,而Thread.sleep()不释放锁

4. synchronized锁升级优化

4.1 偏向锁

  引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

4.1.1 偏向锁获取过程

当线程A第一次进入synchronized的同步代码块之内,发现Mark Word的最后三位是001,表示当前无锁状态,于是选择代价最小的方式加了个偏向锁只在第一次获取偏向锁的时候执行CAS操作将自己的线程Id通过CAS操作设置到Mark Word中),同时将偏向锁标志位改为1后面如果自己再获取锁的时候,每次检查一下发现自己之前加了偏向锁,就直接执行代码,就不需要再次加锁了。加了偏向锁的线程是个自私的线程,这家伙用完了锁之后,自己加锁时候修改过的Mark Word信息都不会再改回来了也就是它不会主动释放锁

这个哥们不释放锁,如果它用完了,别人这个时候需要进入synchronized代码块怎么办?此时涉及到偏向锁之重偏向。

4.1.2 偏向锁之重偏向

线程B去申请加锁,发现是线程A加了偏向锁;这时候回去判断一下线程A是否存活,如果线程A挂了就可以重新偏向了重偏向也就是将自己的线程ID设置到Mark Word中

如果线程A没挂,但是synchronized代码块执行完了,这个时候也可以重新偏向了,将偏向标识指向自己。

如果线程B在申请获取锁的时候,线程A这哥们还没执行完synchronized同步代码块怎么办?这就需要将锁升级一下了,都使用偏向锁不行吗?不升级有什么坏处?

4.1.2 偏向锁的释放

偏向锁的释放在上述提到偏向锁不会主动释放锁,只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的释放,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

4.2 轻量级锁

4.2.1 偏向锁为什么要升级为轻量级锁?

轻量级锁模式下,加锁之前会创建一个锁记录,然后将Mark Word中的数据备份到锁记录中(Mark Word存储hashcode、GC年龄等很重要数据,不能丢失了),以便后续恢复Mark Word使用。

这个锁记录放在加锁线程的虚拟机栈中,加锁的过程就是将Mark Word 前面的30位指向锁记录地址。所以mark word的这个地址指向哪个线程的虚拟机栈中,就说明哪个线程获取了轻量级锁

先看如下代码块:

// 代码块1
synchronized(this){// 业务代码1  
}
// 代码块2
synchronized(this){// 业务代码2
}
// 代码块3
synchronized(this){// 业务代码3
}
// 代码块4
synchronized(this){// 业务代码4
}

假如这个时候有线程A、B、C、D四个线程,线程A先加了偏向锁。之前讲过偏向锁只是在第一次获取锁的时候加锁,后面都是直接操作的不需要加锁

这个时候其它几个线程B、C、D想要加锁,如果线程A连续执行上面4个代码块,那么其他线程看到线程A都在执行synchronized同步代码块,没完没了了,想重偏向都不行!! ,这个时候就需要等线程A执行完4个synchronized代码块之后才能获取锁啊,哈哈,别的线程都只能看线程A一个人自己在那表演了,这样代码就变成串行执行了。所以偏向锁需要升级。

(1)首先线程A持有偏向锁,然后正在执行synchronized块中的代码。

(2)这个时候线程B来竞争锁,发现有人加了偏向锁并且正在执行synchronized块中的代码,为了避免上述说的线程A一直持有锁不释放的情况,需要对锁进行升级,升级为轻量级锁。

(3)先将线程A暂停为线程A创建一个锁记录Lock Record,将Mark Word的数据复制到锁记录中;然后将锁记录放入线程A的虚拟机栈中。

(4)然后将Mark Word中的前30位指向线程A中锁记录的地址,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,将线程A唤醒,线程A就知道自己持有了轻量级锁。

4.2.2 在轻量级锁模式下,多线程是怎么竞争锁和释放锁的?

(1)线程A线程B同时竞争锁,在轻量级锁模式下,都会创建Lock Record锁记录放入自己的栈帧中。

(2)同时执行CAS操作将Mark Word前30位设置为自己锁记录的地址谁设置成功了,谁就获取到锁。

4.2.3 轻量级锁模式下获取锁失败的线程应该会怎么样?

获取不到会自旋,回看3.4讲解的:线程获取锁失败后的自旋操作好处

4.2.4 轻量级锁的释放

就将自己的Lock Record中的Mark Word备份的数据恢复回去即可,恢复的时候执行的是CAS操作将Mark Word数据恢复成加锁前的样子。

(1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。

(2)如果替换成功,整个同步过程就完成了。

(3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

4.3 重量级锁、轻量级锁和偏向锁之间转换

4.4 其他优化

4.4.1 适应性自旋(Adaptive Spinning)

从轻量级锁获取的流程中我们知道当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。

4.4.2 锁粗化(Lock Coarsening)

锁粗化的概念应该比较好理解,就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。

4.4.3 锁消除(Lock Elimination)

锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。

4.4.4 总结

  本文重点介绍了JDk中采用轻量级锁和偏向锁等对Synchronized的优化,但是这两种锁也不是完全没缺点的,比如竞争比较激烈的时候,不但无法提升效率,反而会降低效率,因为多了一个锁升级的过程,这个时候就需要通过-XX:-UseBiasedLocking来禁用偏向锁。下面是这几种锁的对比:

优点

缺点

适用场景

偏向锁

加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。

如果线程间存在锁竞争,会带来额外的锁撤销的消耗。

适用于只有一个线程访问同步块场景。

轻量级锁

竞争的线程不会阻塞,提高了程序的响应速度。

如果始终得不到锁竞争的线程使用自旋会消耗CPU。

追求响应时间。 同步块执行速度非常快。

重量级锁

线程竞争不使用自旋,不会消耗CPU。

线程阻塞,响应时间缓慢。

追求吞吐量。 同步块执行速度较长。

 

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

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

相关文章

6.7机器学习期末复习题

空间 样本空间 就是属性的所有可能情况,包括了一切可能出现或不可能出现的所有样本情况 版本空间&假设空间 假设空间就是在样本空间的基础上,给所有属性都加了一个通配符,表示任意即可;以及加上了一个空集,表示…

Qt界面设计时使各控件依据窗口缩放进行栅格布局的方法

图1 最终效果 想要达成上述图片的布局效果,具体操作如下: 新建一窗体: 所需控件如下: Table View控件一个; Group Box控件一个; Push Button控件2个; Horiziontal Spacer控件2个&#xf…

mac安装Pytest、Allure、brew

安装环境 安装pytest 命令 pip3 install pytest 安装allure 命令:brew install allure 好吧 那我们在安装allure之前 我们先安装brew 安装brew 去了官网复制了命令 还是无法下载 如果你们也和我一样可以用这个方法哦 使用国内的代码仓库来执行brew的安装脚本…

数据结构C语言描述5(图文结合)--队列,数组、链式、优先队列的实现

前言 这个专栏将会用纯C实现常用的数据结构和简单的算法;有C基础即可跟着学习,代码均可运行;准备考研的也可跟着写,个人感觉,如果时间充裕,手写一遍比看书、刷题管用很多,这也是本人采用纯C语言…

一篇文章了解机器学习

一篇文章了解机器学习(上) 一、软件版本安装二、数据集的加载三、数据集的切分四、数据特征提取及标准化1、字典数据的特征提取2、文本特征向量的提取3、数据标准化处理 四、特征降维注:训练器的区别::五、模型的训练与…

day03(单片机高级)RTOS

目录 RTOS(实时操作系统) 裸机开发模式 轮询方式 前后台(中断方式) 改进(前后台(中断))定时器 裸机进一步优化 裸机的其他问题 RTOS的概念 什么是RTOS 为什么要使用 RTOS RTOS的应用场景 RTOS的…

Hello-Go

Hello-Go 环境变量 GOPATH 和 GOROOT :不同于其他语言,go中没有项目的说法,只有包,其中有两个重要的路径,GOROOT 和 GOPATH Go开发相关的环境变量如下: GOROOT:GOROOT就是Go的安装目录&…

pytorch官方FasterRCNN代码详解

本博文转自捋一捋pytorch官方FasterRCNN代码 - 知乎 (zhihu.com),增加了其中代码的更详细的解读,以帮助自己理解该代码。 代码理解的参考Faster-RCNN全面解读(手把手带你分析代码实现)---前向传播部分_手把手faster rcnn-CSDN博客 1. 代码结构 作为 to…

全志T113双核异构处理器的使用基于Tina Linux5.0——RTOS系统定制开发

8、RTOS系统定制开发 此处以在rtos/components/aw目录下创建一个简单的软件包为例,帮助客户了解RTOS环境,为RTOS系统定制开发提供基础。 RTOS环境下的软件包主要由三部分组成,源文件,Makefile,Kconfig,如下…

springboot实战(13)(@PatchMapping、@RequestParam、@URL、ThreadLocal线程局部变量)

目录 一、PATCH请求方式。 二、实现用户更新头像功能。 三、注解RequestParam。 四、注解URL。(对传来的参数是否是合法地址进行校验) 一、PATCH请求方式。 patch中文翻译:局部、小块。PATCH 请求主要用于对已存在的资源进行局部修改&#xf…

nvm安装node遇到的若干问题(vscode找不到npm文件、环境变量配置混乱、npm安装包到D盘)

问题一:安装完nvm后需要做哪些环境变量的配置? 1.打开nvm文件夹下的setting文件,设置nvm路径和安装node路径,并添加镜像。 root: D:\software\nvm-node\nvm path: D:\software\nvm-node\nodejs node_mirror: https://npmmirror.c…

面向FWA市场!移远通信高性能5G-A模组RG650V-NA通过北美两大重要运营商认证

近日,全球领先的物联网整体解决方案供应商移远通信宣布,其旗下符合3GPP R17标准的新一代5G-A模组RG650V-NA成功通过了北美两家重要运营商认证。凭借高速度、大容量、低延迟、高可靠等优势,该模组可满足CPE、家庭/企业网关、移动热点、高清视频…

2024年11月21日Github流行趋势

项目名称:twenty 项目维护者:charlesBochet, lucasbordeau, Weiko, FelixMalfait, bosiraphael项目介绍:正在构建一个由社区支持的现代化Salesforce替代品。项目star数:21,798项目fork数:2,347 项目名称:p…

AWTK 最新动态:支持鸿蒙系统(HarmonyOS Next)

HarmonyOS是全球第三大移动操作系统,有巨大的市场潜力,在国产替代的背景下,机会多多,AWTK支持HarmonyOS,让AWTK开发者也能享受HarmonyOS生态的红利。 AWTK全称为Toolkit AnyWhere,是ZLG倾心打造的一套基于C…

docker 配置同宿主机共同网段的IP 同时通过通网段的另一个电脑实现远程连接docker

docker配置网络 #宿主机执行命令 ifconfig 查询对应的主机ip 子网掩码 网关地址 #[网卡名称]:inet[主机IP] netmask[子网掩码] broadcast[网关地址]这里需要重点关注:eno1[网卡名称]以及【192.168.31.225】网关地址 在宿主机执行docker命令创建一个虚拟…

使用 Elastic AI Assistant for Search 和 Azure OpenAI 实现从 0 到 60 的转变

作者:来自 Elastic Greg Crist Elasticsearch 推出了一项新功能:Elastic AI Assistant for Search。你可以将其视为 Elasticsearch 和 Kibana 开发人员的内置指南,旨在回答问题、引导你了解功能并让你的生活更轻松。在 Microsoft AI Services…

React (三)

文章目录 项目地址十二、性能优化12.1 使用useMemo避免不必要的计算12.2 使用memo缓存组件,防止过度渲染12.3 useCallBack缓存函数12.4 useCallBack里访问之前的状态(没懂)十三、Styled-Components13.1 安装13.2给普通html元素添加样式13.3 继承和覆盖样式13.4 给react组件添…

Etcd 框架

基本了解 客户端、长连接与租约的关系 客户端对象 etcd的客户端对象是用户与etcd服务进行交互的主要接口,主要功能就是存储、通知和事务等功能访问 键值存储:客户端通过put 和 get操作存储数据;数据存储在etcd的层级化键值数据库中监听器&a…

IDEA2023 创建SpringBoot项目(一)

一、Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。 二、快速开发 1.打开IDEA选择 File->New->Project 2、…

教育数字化转型新时代:探索智慧学习空间的无限可能

在信息技术的浪潮推动下,教育行业正迎来一场前所未有的变革。这场变革的核心在于教育数字化转型,它要求我们重新审视和构建传统的学习模式,以适应快速变化的社会需求。在这个过程中,智慧学习空间作为数字化转型的重要成果&#xf…