5

Java锁的逻辑(结合对象头和ObjectMonitor) - 大兴神

 2 years ago
source link: https://www.cnblogs.com/soker/p/16926925.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Java锁的逻辑(结合对象头和ObjectMonitor) - 大兴神 - 博客园

我们都知道在Java编程中多线程的同步使用synchronized关键字来标识,那么这个关键字在JVM底层到底是如何实现的呢。
我们先来思考一下如果我们自己实现的一个锁该怎么做呢:

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

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

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

image

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代码处,偏向锁的逻辑如下:

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

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

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

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

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

由于重量级锁需要维护ObjectMonitor,所以性能不如轻量级锁,轻量级锁只需要修改对象头即可,重量级锁不但需要修改对象头还要维护ObjectMonitor的数据结构。
Java程序执行到synchronized代码处,重量级锁的逻辑如下:

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

我们可以看到,重量级锁由于需要维护ObjectMonitor所以性能不高,如果对象能够一直处在轻量级锁的状态下性能会有大幅提升。
同时需要注意,当你在同步代码中调用wait的时候,因为需要维护wait线程队列,轻量级锁需要膨胀为重量级锁。当你调用hashcode方法的时候,偏向锁会膨胀为轻量级锁。具体的锁的切换流程可以看这篇博客《深入理解偏向锁》

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

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

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

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

本文参考书籍:

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

Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK