- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
我们都知道在Java编程中多线程的同步使用synchronized关键字来标识,那么这个关键字在JVM底层到底是如何实现的呢。 我们先来思考一下如果我们自己实现的一个锁该怎么做呢:
第一点Java使用对象头来维护对象的上锁状态,第二点Java使用ObjectMonitor来维护等待中的线程及持有锁的线程****.
对象头中记录了锁的状态,Java中现在有三种锁状态偏向锁、轻量级锁、重量级锁。其中重量级锁就是用来和ObjectMonitor进行关联的,最开始Java只有重量级锁,但是重量级锁在有锁竞争的情况下需要阻塞线程,同时需要对ObjectMonitor的数据结构进行操作,比较耗费性能。后来Java为了提高锁的性能,引入了偏向锁和轻量级锁。 这里需要注意偏向锁和轻量级锁与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中synchronized关键字上锁的的逻辑,这里的细节有很多,我们只描述大概的过程。 同时我们还要注意对象头中存储的hashcode的变化,对象刚开始创建的时候对象头中的hashcode还未生成,只有程序调用hashcode方法时候才会将hashcode存储到对象头中,这样可以保证不管用什么hashcode算法,同一个对象的hashcode在他的生命周期中都不会改变。 这里强调一下,如果对象处在重量级锁的时候,它就无法再次进入到轻量级锁状态,如果对象处在轻量级锁,它就无法进入到偏向锁的状态。只能等待对象进入无锁状态之后,再次进行判断.
Java程序执行到synchronized代码处,偏向锁的逻辑如下:
我们可以看到,一段同步代码如果一直是由一个线程执行的时候,这个线程只需要做2和3中简单的判断就可继续往下执行同步代码,最初的性能消耗只是第一次上锁的时候需要修改对象头。这就是偏向锁的作用,可以大幅度提升synchronized锁的效率。 但是由于底层为了实现偏向锁的逻辑过于复杂,在JDK15之后已经默认关闭偏向锁了,在现代的程序中同一个线程一直持有一个锁的情况已经不多了。 具体的锁的切换流程可以看这篇博客 《深入理解偏向锁》 .
Java程序执行到synchronized代码处,轻量级锁的逻辑如下:
我们可以看到,轻量级锁如果锁的竞争比较低(线程比较少,同步程序执行速度较快)的情况下,线程可以不需要进入到阻塞状态,通过自旋等待锁的释放。同时轻量级锁也不需要维护ObjectMonitor的数据,进一步提升了性能.
由于重量级锁需要维护ObjectMonitor,所以性能不如轻量级锁,轻量级锁只需要修改对象头即可,重量级锁不但需要修改对象头还要维护ObjectMonitor的数据结构。 Java程序执行到synchronized代码处,重量级锁的逻辑如下:
我们可以看到,重量级锁由于需要维护ObjectMonitor所以性能不高,如果对象能够一直处在轻量级锁的状态下性能会有大幅提升。 同时需要注意,当你在同步代码中调用wait的时候,因为需要维护wait线程队列,轻量级锁需要膨胀为重量级锁。当你调用hashcode方法的时候,偏向锁会膨胀为轻量级锁。具体的锁的切换流程可以看这篇博客 《深入理解偏向锁》 .
不过这里我有一个疑问,就是ObjectMonitor是如何和对象做关联的,即重量级锁修改对象头的时候,对象对应的ObjectMonitor对象的内存地址是怎么找到的,难道底层维护了一个ObjectMonitor的Map?我查了些资料和书籍都没说明.
我们可以看到当遇到synchronized代码块的时候,对象头可能处于偏向锁、轻量级锁、重量级锁三种状态,这三种锁各有各的特点.
锁 | 优势 | 劣势 | 触发场景 |
---|---|---|---|
偏向锁 | 只需要修改一次对象头 | 不支持调用hashcode方法,如果线程存在竞争,需要额外撤销锁,底层代码维护困难 | 单个线程长期重复持有锁 |
轻量级锁 | 自旋无需阻塞线程,减少线程上下文切换 | 如果始终获取不到锁,自旋会消耗cpu资源(感觉也不算缺点,高并发下对象会一直处在重量级锁的状态下,执行重量级锁的逻辑即可) | 少量线程交替持有锁 |
重量级锁 | 可以执行wait等操作 | 线程会阻塞,同时需要维护ObjectMonitor性能低 | 大量线程同时争抢锁 |
毕竟大量线程同时争抢锁的情况不多,如果对象一直处在轻量级锁的状态下,锁的性能已经非常高,与JDK中的Lock的性能已经相差无几,因为Lock的底层也是使用CAS算法来维护锁的状态.
本文参考书籍:
- 《Java并发编程的艺术》这本书值得一读,底层原理讲的比较深入。
最后此篇关于Java锁的逻辑(结合对象头和ObjectMonitor)的文章就讲到这里了,如果你想了解更多关于Java锁的逻辑(结合对象头和ObjectMonitor)的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我是一名优秀的程序员,十分优秀!