首页 >> 大全

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

2023-07-25 大全 29 作者:考证青年

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

我们先来思考一下如果我们自己实现的一个锁该怎么做呢:

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

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

对象头

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

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

//结构体如下
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中关键字上锁的的逻辑,这里的细节有很多,我们只描述大概的过程。

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

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

偏向锁

java锁的是对象_对象逻辑图_

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

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

对象初始化时候,JVM为了优化偏向锁,会使对象头会处在匿名偏向状态,对象头的数据结构是偏向锁的数据结构,但是是0,称作状态,即可偏向状态。如果线程获取了偏向锁,不为空,则进入状态,即偏向状态。

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

轻量级锁

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

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

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

重量级锁

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

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

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

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

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

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

对象逻辑图__java锁的是对象

总结

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

锁优势劣势触发场景

偏向锁

只需要修改一次对象头

不支持调用方法,如果线程存在竞争,需要额外撤销锁,底层代码维护困难

单个线程长期重复持有锁

轻量级锁

自旋无需阻塞线程,减少线程上下文切换

如果始终获取不到锁,自旋会消耗cpu资源(感觉也不算缺点,高并发下对象会一直处在重量级锁的状态下,执行重量级锁的逻辑即可)

少量线程交替持有锁

重量级锁

可以执行wait等操作

线程会阻塞,同时需要维护性能低

大量线程同时争抢锁

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

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了