Java锁的逻辑(结合对象头和ObjectMonitor)
作者:互联网
我们都知道在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,如果成功了就获取到锁,直接执行同步代码。如果失败的话说明有线程获取了偏向锁,此时线程会请求那个持有锁的线程释放锁。
- 如果持有锁的线程还在同步代码中,则无法释放锁,这个时候锁会膨胀为轻量级锁。膨胀的的时候会修改对象头为轻量级锁。
- 同步代码执行完成后,线程并不会重置对象头的数据,即不会释放锁,以便下次再次执行的时候可以直接进入同步代码。