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

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

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

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

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

我们可以看到,一段同步代码如果一直是由一个线程执行的时候,这个线程只需要做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并发编程的艺术》这本书值得一读,底层原理讲的比较深入。

Java锁的逻辑(结合对象头和ObjectMonitor)的更多相关文章

  1. java锁与监视器概念 为什么wait、notify、notifyAll定义在Object中 多线程中篇(九)

    在Java中,与线程通信相关的几个方法,是定义在Object中的,大家都知道Object是Java中所有类的超类 在Java中,所有的类都是Object,借助于一个统一的形式Object,显然在有些处 ...

  2. Java锁的升级策略 偏向锁 轻量级锁 重量级锁

    这三种锁是指锁的状态,并且是专门针对Synchronized关键字.JDK 1.6 为了减少"重量级锁"的性能消耗,引入了"偏向锁"和"轻量级锁&qu ...

  3. 015-线程同步-synchronized几种加锁方式、Java对象头和Monitor、Mutex Lock、JDK1.6对synchronized锁的优化实现

    一.synchronized概述基本使用 为确保共享变量不会出现并发问题,通常会对修改共享变量的代码块用synchronized加锁,确保同一时刻只有一个线程在修改共享变量,从而避免并发问题. syn ...

  4. java对象头信息和三种锁的性能对比

    java头的信息分析 首先为什么我要去研究java的对象头呢? 这里截取一张hotspot的源码当中的注释 这张图换成可读的表格如下 |-------------------------------- ...

  5. 并发王者课-青铜5:一探究竟-如何从synchronized理解Java对象头中的锁

    在前面的文章<青铜4:synchronized用法初体验>中,我们已经提到锁的概念,并指出synchronized是锁机制的一种实现.可是,这么说未免太过抽象,你可能无法直观地理解锁究竟是 ...

  6. Java对象头与锁

    对象由多部分构成的,对象头,属性字段.补齐区域等.所谓补齐区域是指如果对象总大小不是4字节的整数倍,会填充上一段内存地址使之成为整数倍. 后面两个很好理解,今天我主要想总结一下对象头: 对象头这部分在 ...

  7. JVM源码分析之Java对象头实现

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 “365篇原创计划”第十一篇. 今天呢!灯塔君跟大家讲: JVM源码分析之Java对象头实现 HotSpot虚拟机中,对象在内存中的布局分为三 ...

  8. Java锁 到底锁的是哪个对象?

    更新:在一次和一位专家的交谈中,他对一下代码能否能够成功同步,给予了否定的答案, 他的理由是”以构造函数的成员变量作为synchronized的锁,在多线程的情况下,每一个线程都持有自己私有变量的锁, ...

  9. Java编程的逻辑 (71) - 显式锁

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  10. 深入理解多线程(三)—— Java的对象头

    上一篇文章中我们从HotSpot的源码入手,介绍了Java的对象模型.这一篇文章在上一篇文章的基础上再来介绍一下Java的对象头.主要介绍一下对象头的作用,结构以及他和锁的关系. Java对象模型回顾 ...

随机推荐

  1. 运维利器-ClusterShell

    前言 和ansible类似,但是更加高效 安装 yum install -y clustershell clush命令: clush -a 全部 等于 clush -g all clush -g 指定 ...

  2. x-pack设置完毕后,es-head无法登陆的问题, 登录需要账号密码的问题

    在elasticsearch.yml中添加如下三行配置 http.cors.enabled: true http.cors.allow-origin: "*" http.cors. ...

  3. R语言、02 案例2-1 Pelican商店、《商务与经济统计》案例题

    编程教材 <R语言实战·第2版>Robert I. Kabacoff 课程教材<商务与经济统计·原书第13版> (安德森) P48.案例2-1 Pelican 商店 PS C: ...

  4. js对象结构赋值const {XXX } =this

    样例1: const { xxx } = this.state; 上面的写法是es6的写法,其实就相当于: const xxx = this.state.xxx 样例2: const {comment ...

  5. 企业运维 | MySQL关系型数据库在Docker与Kubernetes容器环境中快速搭建部署主从实践

    [点击 关注「 WeiyiGeek」公众号 ] 设为「️ 星标」每天带你玩转网络安全运维.应用开发.物联网IOT学习! 希望各位看友[关注.点赞.评论.收藏.投币],助力每一个梦想. 本章目录 目录 ...

  6. C#中ref和out关键字的应用以及区别

    首先:两者都是按地址传递的,使用后都将改变原来参数的数值. 其次:ref可以把参数的数值传递进函数,但是out是要把参数清空,就是说你无法把一个数值从out传递进去的,out进去后,参数的数值为空,所 ...

  7. C++ 高级数据类型(六)—— 自定义数据类型

    转载:https://blog.csdn.net/zjy900507/article/details/79623829 定义自己的数据类型 (typedef) C++ 允许我们在现有数据类型的基础上定 ...

  8. 利用POI遍历出层级结构的excel表格

    import java.util.ArrayList; import java.util.List; import org.apache.poi.ss.util.CellRangeAddress; p ...

  9. springMVC必要jar包

    spring-aop-4.3.2.RELEASE.jar :: 包含在应用中使用Spring 的AOP 特性时所需的类和源码级元数据支持. spring-beans-4.3.2.RELEASE.jar ...

  10. C#中winform DataGridView常用修改点

    1.修改列名 一般情况下,从数据库里面读取的列名是英文或者拼音,但是,有时候显示需要中文,这样就需要修改列名了. dgv.Columns[0].HeaderCell.Value="编号&qu ...