首页IT科技对象锁和类锁的区别(Java锁的逻辑(结合对象头和ObjectMonitor))

对象锁和类锁的区别(Java锁的逻辑(结合对象头和ObjectMonitor))

时间2025-09-18 23:44:18分类IT科技浏览6085
导读:我们都知道在Java编程中多线程的同步使用synchronized关键字来标识,那么这个关键字在JVM底层到底是如何实现的呢。 我们先来思考一下如果我们自己实现的一个锁该怎么做呢: 首先肯定要有个标记记录对象是否已经上锁,执行同步代码之前判断这个标志,如果对象已经上锁线程就阻塞等待锁的释放。...

我们都知道在Java编程中多线程的同步使用synchronized关键字来标识                  ,那么这个关键字在JVM底层到底是如何实现的呢                 。

我们先来思考一下如果我们自己实现的一个锁该怎么做呢: 首先肯定要有个标记记录对象是否已经上锁                          ,执行同步代码之前判断这个标志         ,如果对象已经上锁线程就阻塞等待锁的释放                           。 其次要有一个结构体来维护这些等待中的线程         ,锁释放后来遍历这些线程让他们去抢锁         。

第一点Java使用对象头来维护对象的上锁状态                          ,第二点Java使用ObjectMonitor来维护等待中的线程及持有锁的线程****                 。

对象头

对象头中记录了锁的状态                 ,Java中现在有三种锁状态偏向锁                 、轻量级锁                           、重量级锁                          。其中重量级锁就是用来和ObjectMonitor进行关联的         ,最开始Java只有重量级锁                           ,但是重量级锁需要对ObjectMonitor的数据结构进行操作                 ,比较耗费性能         。后来Java为了提高锁的性能,引入了偏向锁和轻量级锁         。这里需要注意偏向锁和轻量级锁与ObjectMonitor没有任何关联                           ,后面会做详细介绍                          。

ObjectMonitor

Java会为每一个对象和对象的Class对象分配一个ObjectMonitor对象                          ,他是一个C++结构体,ObjectMonitor用来维护当前持有锁的线程                  ,阻塞等待锁释放的线程链表                          ,调用了wait阻塞等待notify的线程链表                  。这里不做过多描述         ,具体的维护逻辑可以搜索其他博客         。

//结构体如下 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 ; }

Java中的锁的逻辑

下面来描述一下Java中synchronized关键字上锁的的逻辑         ,这里的细节有很多         ,我们只描述大概的过程                          。

同时我们还要注意对象头中存储的hashcode的变化                          ,对象刚开始创建的时候对象头中的hashcode还未生成                 ,只有程序调用hashcode方法时候才会将hashcode存储到对象头中         ,这样可以保证不管用什么hashcode算法                           ,同一个对象的hashcode在他的生命周期中都不会改变                  。

这里强调一下                 ,如果对象处在重量级锁的时候,它就无法再次进入到轻量级锁状态                           ,如果对象处在轻量级锁                          ,它就无法进入到偏向锁的状态。只能等待对象进入无锁状态之后,再次进行判断                          。 偏向锁

Java程序执行到synchronized代码处                  ,偏向锁的逻辑如下:

检查对象头中的hashcode是否生成                          ,生成过hashcode的对象无法进入偏向锁(这是因为偏向锁设计时         ,没有地方用来备份hashcode)                           。 检查对象头中的锁标志位是否是01                  ,如果不是说明对象处在其他锁的状态                          ,则执行其他锁的逻辑。 如果偏向锁的线程ID是自己的线程ID则直接执行同步代码块         ,说明之前此线程已经获取到了锁                 。 如果偏向锁ID不是自己的线程ID         ,通过CAS算法尝试偏向锁的线程ID                          ,如果成功了就获取到锁                 ,直接执行同步代码                           。如果失败的话说明有线程获取了偏向锁         ,此时线程会请求那个持有锁的线程释放锁         。 如果持有锁的线程还在同步代码中                           ,则无法释放锁                 ,这个时候锁会膨胀为轻量级锁                 。膨胀的的时候会修改对象头为轻量级锁                          。 同步代码执行完成后,线程并不会重置对象头的数据                           ,即不会释放锁                          ,以便下次再次执行的时候可以直接进入同步代码         。

我们可以看到,一段同步代码如果一直是由一个线程执行的时候                  ,这个线程只需要做2和3中简单的判断就可继续往下执行         。这就是偏向锁的作用                          ,可以大幅度提升synchronized锁的效率                          。但是由于底层为了实现偏向锁的逻辑过于复杂         ,在JDK15之后已经默认关闭偏向锁了                  ,在现代的程序中同一个线程一直持有一个锁的情况已经不多了                  。具体的锁的切换流程可以看这篇博客《深入理解偏向锁》         。

轻量级锁

Java程序执行到synchronized代码处                          ,轻量级锁的逻辑如下:

检查对象头锁标志位是否是01         ,将对象头复制到栈中进行备份 尝试使用CAS算法修改对象头(这里为了防止其他线程同时和当前线程都去修改对象头抢锁)         ,这时候对象头指向的是当前的栈地址                          ,如果修改成功则获取到锁执行同步代码                          。 如果修改失败                 ,说明其他线程优先获取到了锁         ,当前线程自旋(循环)获取锁                           ,超过一定的次数后如果还是无法获取到锁                 ,则锁膨胀为重量级锁,膨胀的时候会修改对象头和维护ObjectMonitor的数据结构                  。 同步代码执行完成之后                           ,CAS把备份的对象头写回到对象头中。如果修改失败说明锁已经膨胀为重量级锁了                          ,则执行重量级锁的锁释放逻辑                          。

我们可以看到,轻量级锁如果锁的竞争比较低(线程比较少                  ,同步程序执行速度较快)的情况下                          ,线程可以不需要进入到阻塞状态         ,通过自旋等待锁的释放                           。同时轻量级锁也不需要维护ObjectMonitor的数据                  ,进一步提升了性能。

重量级锁

由于重量级锁需要维护ObjectMonitor                          ,所以性能不如轻量级锁         ,轻量级锁只需要修改对象头即可         ,重量级锁不但需要修改对象头还要维护ObjectMonitor的数据结构                 。

Java程序执行到synchronized代码处                          ,重量级锁的逻辑如下: 通过对象头中的ObjectMonitor的引用地址                 ,找到ObjectMonitor对象         ,此时ObjectMonitor中存储了无锁状态下对象头的备份                           。 判断_owner是否是当前线程                           ,如果不是则说明锁被其他线程持有                 ,则阻塞当前线程(阻塞的逻辑应该和LockSupport.park()的逻辑是一样的),并把当前线程加入到阻塞链表中         。 如果_owner是当前线程                           ,则_recursions加1记录重入次数(比如递归的时候会重复获取锁)                          ,并执行同步代码                 。 同步代码执行完成后,_recursions减1(因为重量级锁是可重入锁                  ,退出的时候可能退出多次)                          ,唤醒阻塞链表中的线程去抢锁                          。如果没有线程等待则修改对象头为无锁状态         ,把备份的对象头数据写回到对象头         。这里注意                  ,持有锁的时候如果调用hascode方法                          ,修改应该也是备份的对象头中的数据         。

我们可以看到         ,重量级锁由于需要维护ObjectMonitor所以性能不高         ,如果对象能够一直处在轻量级锁的状态下性能会有大幅提升                          。

同时需要注意                          ,当你在同步代码中调用wait的时候                 ,因为需要维护wait线程队列         ,轻量级锁需要膨胀为重量级锁                  。当你调用hashcode方法的时候                           ,偏向锁会膨胀为轻量级锁         。具体的锁的切换流程可以看这篇博客《深入理解偏向锁》                          。

不过这里我有一个疑问                 ,就是ObjectMonitor是如何和对象做关联的,即重量级锁修改对象头的时候                           ,对象对应的ObjectMonitor对象的内存地址是怎么找到的                          ,难道底层维护了一个ObjectMonitor的Map?我查了些资料和书籍都没说明                  。

总结

我们可以看到当遇到synchronized代码块的时候,对象头可能处于偏向锁         、轻量级锁                 、重量级锁三种状态                  ,这三种锁各有各的特点。

锁 优势 劣势 触发场景 偏向锁 只需要修改一次对象头 不支持调用hashcode方法                          ,如果线程存在竞争         ,需要额外撤销锁                  ,底层代码维护困难 单个线程长期重复持有锁 轻量级锁 自旋无需阻塞线程                          ,减少线程上下文切换 如果始终获取不到锁         ,自旋会消耗cpu资源(感觉也不算缺点         ,高并发下对象会一直处在重量级锁的状态下                          ,执行重量级锁的逻辑即可) 少量线程交替持有锁 重量级锁 可以执行wait等操作 线程会阻塞                 ,同时需要维护ObjectMonitor性能低 大量线程同时争抢锁

毕竟大量线程同时争抢锁的情况不多         ,如果对象一直处在轻量级锁的状态下                           ,锁的性能已经非常高                 ,与JDK中的Lock的性能已经相差无几,因为Lock的底层也是使用CAS算法来维护锁的状态                          。

本文参考书籍:

《Java并发编程的艺术》这本书值得一读                           ,底层原理讲的比较深入                           。

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
回退到windows10的上一个版本会删除文件吗苹果(回退到win10后仍然会下载win11的更新)