本篇文章主要从字节码和JVM底层来分析synchronized实现原理和锁升级过程,其中涉及到了简单认识字节码、对象内部结构以及ObjectMonitor等知识点。
阅读本文之前,如果大家对synchronized关键字的基本使用还不是很了解的话,推荐阅读笔者之前的一遍关于synchronized关键字使用的文章:
synchronized三种使用方式都不知道还想通过面试,门都没有
从字节码角度分析synchronized实现
从JVM规范中可以了解到,无论是synchronized修饰方法(实例/静态方法)还是代码块都是基于进入(entry)和退出(exit)monitor对象来实现,但是两种修饰方式在字节码层面实现上有着很大区别。下面我们通过javap -verbose XXX.class命令查看class文件信息来具体分析两者实现上的差异。
synchronized修饰代码块:
程序源码如下:
class文件信息如下:
由上面的class信息可以得知,使用synchronized修饰代码块会在同步代码块之前加monitorenter指令,同时在代码块正常退出(15行)和异常退出(21行)的地方插入monitorexit指令,从而保证monitorenter和monitorexit的成对执行(保证同步代码块执行结束的同时释放锁资源)。可以把monitorenter看作lock.lock(),monitorexit看作lock.unlock(),那么monitorenter和monitorexit可以用更加方便理解的伪代码表示,如下:
synchronized修饰方法:
程序源码如下:
class文件信息如下:
由上面的class信息可以得知,synchronized修饰方法并没有通过插入monitorentry和monitorexit指令来实现,而是在方法表结构中的访问标志(access_flags)设置ACC_SYNCHRONIZED标志来实现。线程在执行方法前先判断access_flags是否标记ACC_SYNCHRONIZED,如果标记则在执行方法前先去获取monitor对象,获取成功则执行方法代码且执行完毕后释放monitor对象,获取失败则表示monitor对象被其他线程获取从而阻塞当前线程。
对象头和MarkWord
说到对象头,我们需要先整体了解下对象的内部结构,如下图所示:
由图可知对象内部结构分为:对象头、实例数据、对齐填充(保证8个字节的倍数)。对象头分为对象标记(markOop)和类元信息(klassOop)。类元信息存储的是指向该对象类元数据(klass)的首地址,4个字节。
对象标记(markOop)是我们重要要介绍的,它存储对象本身运行时的数据,如哈希码、GC标记、锁信息、线程关联等(64位JVM占8个字节,32位JVM占4个字节),称为"Mark Word",存储格式非规定与具体JVM实现有关。
Hotspot JVM中MarkWord存储格式如下:
32位存储格式:
64位存储格式:
由MarkWord存储格式可以了解到JVM可以通过锁标志位来判断锁类型,进而进行处理。注意JDK1.6之前只有重量级锁的,JDK1.6之后才有了偏向锁和轻量级锁,后面锁升级部分会详细讲解。
ObjectMonitor
在JVM的规范中,有这么一些话:“在JVM中,每个对象和类在逻辑上都是和一个监视器相关联的,为了实现监视器的排他性监视能力,JVM为每一个对象和类都关联一个锁,锁住了一个对象,就是获得对象相关联的监视器”。这里的监视器就是指的是ObjectMonitor。
ObjectMonitor在JVM源码中的定义如下:
MarkWord中重量级锁指向的重量级指针就是ObjectMonitor对象指针,是基于操作系统互斥(mutex)实现的。
synchronized锁升级和实现原理
synchronized在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的MarkWord来实现的。JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了无锁->偏向锁->轻量级锁->重量级锁的升级过程,而不是无论什么情况都使用重量级锁。
锁升级的优化是针对于不同同步场景进行的优化,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁,存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效的,但是如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁。
下面结合上图所示的MarkWord对几种锁类型进行介绍:
- 无锁:MarkWord标志位01,没有线程执行同步方法/代码块时的状态。
- 偏向锁:MarkWord标志位01(和无锁标志位一样)。偏向锁是通过在bitfields中通过CAS设置当前正在执行的ThreadID来实现的。假设线程A获取偏向锁执行代码块(即对象头设置了ThreadA_ID),线程A同步块未执行结束时,线程B通过CAS尝试设置ThreadB_ID会失败,因为存在锁竞争情况,这时候就需要升级为轻量级锁。注:偏向锁是针对于不存在资源抢占情况时候使用的锁,如果被synchronized修饰的方法/代码块竞争线程多可以通过禁用偏向锁来减少一步锁升级过程。可以通过JVM参数-XX:-UseBiasedLocking = false来关闭偏向锁。
- 轻量级锁:MarkWord标志位00。轻量级锁是采用自旋锁的方式来实现的,自旋锁分为固定次数自旋锁和自适应自旋锁。 轻量级锁是针对竞争锁对象线程不多且线程持有锁时间不长的场景, 因为阻塞线程需要CPU从用户态转到内核态,代价很大,如果一个刚刚阻塞不久就被释放代价有大。具体实现和升级为重量级锁过程:线程A获取轻量级锁时会把对象头中的MarkWord复制一份到线程A的栈帧中创建用于存储锁记录的空间DisplacedMarkWord,然后使用CAS将对象头中的内容替换成线程A存储DisplacedMarkWord的地址。如果这时候出现线程B来获取锁,线程B也跟线程A同样复制对象头的MarkWord到自己的DisplacedMarkWord中,如果线程A锁还没释放,这时候那么线程B的CAS操作会失败,会继续自旋,当然不可能让线程B一直自旋下去,自旋到一定次数(固定次数/自适应)就会升级为重量级锁。
- 重量级锁:通过对象内部监视器(monitor)实现,monitor本质前面也提到了是基于操作系统互斥(mutex)实现的,操作系统实现线程之间切换需要从用户态到内核态切换,成本非常高。
注:锁只可以升级不可以降级,但是偏向锁可以被重置为无锁状态。
最后,附上一张关于synchronized锁升级流程图(很全面很牛):
注:由于文章中上传的图片会被压缩,清晰度受到影响,可以关注并私信作者"锁升级"获取synchronized锁升级流程图(高清版)。
END
笔者是一位热爱互联网、热爱互联网技术、热于分享的年轻人,如果您跟我一样,我愿意成为您的朋友,分享每一个有价值的知识给您。喜欢作者的同学,点赞+转发+关注哦!
点赞+转发+关注,私信作者“读书笔记”即可获得BAT大厂面试资料、高级架构师VIP视频课程等高质量技术资料。