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。

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

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

 

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

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

相关文章

Linux 进程概念与进程状态

目录 1. 冯诺依曼体系结构2. 操作系统(Operator System)2.1 概念2.2 设计OS的目的2.3 系统调用和库函数概念 3. 进程概念3.1 描述进程 - PCB3.2 task_struct3.3 查看进程3.4 通过系统调用获取进程标识符PID, PPID3.5 通过系统调用创建fork 4.…

计算机网络(14)ip地址超详解

先看图: 注意看第三列蓝色标注的点不会改变,A类地址第一个比特只会是0,B类是10,C类是110,D类是1110,E类是1111. IPv4地址根据其用途和网络规模的不同,分为五个主要类别(A、B、C、D、…

shell脚本启动springboot项目

nohup java -jar springboot.jar > springboot.log 2>&1 & 表示日志输出重定向到springboot.log日志文件, 而原本的日志继续输出到 项目同级的log文件夹下, 所以这个重定向没必要. 我们没必要要2分日志 #!/bin/bash# 获取springboot项目的进程ID PID$(ps -e…

51c大模型~合集76

我自己的原文哦~ https://blog.51cto.com/whaosoft/12617524 #诺奖得主哈萨比斯新作登Nature,AlphaQubit解码出更可靠量子计算机 谷歌「Alpha」家族又壮大了,这次瞄准了量子计算领域。 今天凌晨,新晋诺贝尔化学奖得主、DeepMind 创始人哈萨…

FileProvider高版本使用,跨进程传输文件

高版本的android对文件权限的管控抓的很严格,理论上两个应用之间的文件传递现在都应该是用FileProvider去实现,这篇博客来一起了解下它的实现原理。 首先我们要明确一点,FileProvider就是一个ContentProvider,所以需要在AndroidManifest.xml里面对它进行声明: <provideran…

【Java】二叉树:数据海洋中灯塔式结构探秘(上)

个人主页 &#x1f339;&#xff1a;喜欢做梦 二叉树中有一个树&#xff0c;我们可以猜到他和树有关&#xff0c;那我们先了解一下什么是树&#xff0c;在来了解一下二叉树 一&#x1f35d;、树型结构 1&#x1f368;.什么是树型结构&#xff1f; 树是一种非线性的数据结构&…

网口输出的加速度传感器

一、功能概述 1.1 设备简介 本模块为了对电机、风机、水泵等旋转设备进行预测性运维而开发&#xff0c;只需一个模块&#xff0c; 就可以采集旋转设备的 3 路振动信号&#xff08;XYZ 轴&#xff09;和一路温度信号&#xff0c;防护等级 IP67 &#xff0c;能够 适应恶劣的工业…

力扣面试经典 150(上)

文章目录 数组/字符串1. 合并两个有序数组2. 移除元素3. 删除有序数组中的重复项4. 删除有序数组的重复项II5. 多数元素6. 轮转数组7. 买卖股票的最佳时机8. 买卖股票的最佳时机II9. 跳跃游戏10. 跳跃游戏II11. H 指数12. O(1)时间插入、删除和获取随机元素13. 除自身以外数组的…

浅谈 proxy

应用场景 Vue2采用的defineProperty去实现数据绑定&#xff0c;Vue3则改为Proxy&#xff0c;遇到了什么问题&#xff1f; - 在Vue2中不能检测数组和对象的变化 1. 无法检测 对象property 的添加或移除 var vm new Vue({data:{a:1} })// vm.a 是响应式的vm.b 2 // vm.b 是…

P4-1【应用数组进行程序设计】第一节——知识要点:一维数组

视频&#xff1a; P4-1【应用数组进行程序设计】第一节——知识要点&#xff1a;一维数组 项目四 应用数组进行程序设计 任务一&#xff1a;冒泡排序 知识要点&#xff1a;一维数组 目录 一、任务分析 二、必备知识与理论 三、任务实施 一、任务分析 用冒泡法对任意输入…

【数据库入门】关系型数据库入门及SQL语句的编写

1.数据库的类型&#xff1a; 数据库分为网状数据库&#xff0c;层次数据库&#xff0c;关系型数据库和非关系型数据库四种。 目前市场上比较主流的是&#xff1a;关系型数据库和非关系型数据库。 关系型数据库使用结构化查询语句&#xff08;SQL&#xff09;对关系型数据库进行…

day07(单片机高级)继电器模块绘制

目录 继电器模块绘制 原理图 布局 添加板框 布线 按tab修改线宽度 布线换层 泪滴 铺铜 铺铜的作用 铺铜的使用规范 添加丝印 步骤总结 继电器模块绘制 到淘宝找一个继电器模块 继电器模块的使用&#xff08;超详细&#xff09;_继电器模块工作原理-CSDN博客文章浏览阅读4.8w次&…

1+X应急响应(网络)病毒与木马的处置:

病毒与木马的处置&#xff1a; 病毒与木马的简介&#xff1a; 病毒和木马的排查与恢复&#xff1a;

【电路笔记 TMS320F28335DSP】时钟+看门狗+相关寄存器(功能模块使能、时钟频率配置、看门狗配置)

时钟源和主时钟&#xff08;SYSCLKOUT&#xff09; 外部晶振&#xff1a;通常使用外部晶振&#xff08;如 20 MHz&#xff09;作为主要时钟源。内部振荡器&#xff1a;还可以选择内部振荡器&#xff08;INTOSC1 和 INTOSC2&#xff09;&#xff0c;适合无需高精度外部时钟的应…

CCE-基础

背景&#xff1a; 虚拟化产生解决物理机资源浪费问题&#xff0c;云计算出现实现虚拟化资源调度和管理&#xff0c;容器出现继续压榨虚拟化技术产生的资源浪费&#xff0c;用命名空间隔离&#xff08;namespace&#xff09; 灰度升级&#xff08;升级中不影响业务&#xff09…

基于LLama_factory的Qwen2.5大模型的微调笔记

Qwen2.5大模型微调记录 LLama-facrotyQwen2.5 模型下载。huggingface 下载方式Modelscope 下载方式 数据集准备模型微调模型训练模型验证及推理模型导出 部署推理vllm 推理Sglang 推理 LLama-facroty 根据git上步骤安装即可&#xff0c;要求的软硬件都装上。 llama-factory运行…

提取图片高频信息

提取图片高频信息 示例-输入&#xff1a; 示例-输出&#xff1a; 代码实现&#xff1a; import cv2 import numpy as npdef edge_calc(image):src cv2.GaussianBlur(image, (3, 3), 0)ddepth cv2.CV_16Sgray cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)grad_x cv2.Scharr(g…

移动充储机器人“小奥”的多场景应用(上)

一、高速公路服务区应用 在高速公路服务区&#xff0c;新能源汽车的充电需求得到“小奥”机器人的及时响应。该机器人配备有储能电池和自动驾驶技术&#xff0c;能够迅速定位至指定充电点&#xff0c;为待充电的新能源汽车提供服务。得益于“小奥”的机动性&#xff0c;其服务…

怎么只提取视频中的声音?从视频中提取纯音频技巧

在数字媒体的广泛应用中&#xff0c;提取视频中的声音已成为一项常见且重要的操作。无论是为了学习、娱乐、创作还是法律用途&#xff0c;提取声音都能为我们带来诸多便利。怎么只提取视频中的声音&#xff1f;本文将详细介绍提取声音的原因、工具、方法以及注意事项。 一、为什…

Windows环境GeoServer打包Docker极速入门

目录 1.前言2.安装Docker3.准备Dockerfile4.拉取linux环境5.打包镜像6.数据挂载6.测试数据挂载7.总结 1.前言 在 Windows 环境下将 GeoServer 打包为 Docker&#xff0c;可以实现跨平台一致性、简化环境配置、快速部署与恢复&#xff0c;同时便于扩展集成和版本管理&#xff0c…