synchronized实现原理

Java对象头

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示:
image.png

  1. 实例数据:存放类的属性数据信息,包括父类的属性信息
  2. 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐
  3. 对象头:Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度

对象头如下图所示(以32位虚拟机为例):
image.png
image.png

  • Mark Word:存储自身的运行时数据,例如 HashCode、GC 年龄、锁相关信息等内容。
  • Klass Word:类型指针指向它的类元数据的指针。

而其中,Mark Word 的组成又如下图所示:
image.png
其中,State是锁的标志位,如下所示:
image.png

Monitor

Monitor对象并不是Java语言层面的对象,它是操作系统层面的对象,采用C++语言实现。每个Java对象都可以关联到一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级锁)之后,根据上面的图片,改对象的对象头中的 Mark Word 中就会设置为指向 Monitor 对象的指针,并且锁标志位置为10。下面是 Monitor 对象的结构,一般有3个组成部分:WaitSet、EntryList、Owner。

image.png
其中,Owner主要是是 Monitor 的所有者,如上图所示的 Thread-2;而 EntryList 列表是存放想要成为 Owner 但是 Owner 已经有线程占用了,就会进入 EntryList 列表;而 WaitSet 则是存放竞争成为 Owner 但是处在 WAITTING 状态的线程。
image.png
下面是字节码层面对 synchronized 重量级锁实现的解释:
image.png
image.png
其中说明一下 monitorexit 和 monitorenter 这两条指令:

  1. MonitorEnter指令:插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁
  2. MonitorExit指令:插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit,以保证对应对象的对象头的Mark Word可以恢复为无状态的锁状态。

    synchronized优化

    轻量级锁

    image.png

    image.png
    image.png
    image.png
    image.png
    image.png
    image.png
    image.png

    锁膨胀

    image.png
    image.png
    image.png
    image.png

    自旋优化

    image.png
    image.png
    image.png

    偏向锁

    image.png
    image.png
    image.png

    总结

    总的来说,偏向锁、轻量级锁、重量级锁都是synchronized关键字去实现的,但是它们的底层实现原理不尽相同,并且锁是按照“偏向锁 -> 轻量级锁 -> 重量级锁”的顺序升级,如图所示:
    image.png
    关于是什么时候升级锁、各种锁的优缺点,下面这篇文章总结的非常好:
    https://xiaomi-info.github.io/2020/03/24/synchronized/