目录

对象在内存中是如何布局的

如何查看对象在内存中的布局

markword数据结构

加锁后发生了什么

偏向锁

什么是偏向锁

偏向锁定时hashCode 哪去了?

为什么需要偏向锁

为什么从JDK15开始默认关闭偏向锁

什么是安全点

JDK8 为什么要延迟4S后开启偏向锁

锁升级流程

轻量级锁(Thin Lock)

自旋锁

自适应自旋锁

重量级锁(Fat Lock)

Synchronized 锁实现

监视器锁 (monitor)

总结


对象在内存中是如何布局的

在聊到对象加锁这个话题,那就必须先聊聊对象在内存中的布局, 你知道一个对象在内存中是如何布局的吗?一个对象new出来以后,它在内存中主要分为一下四个部分:

markword 这部分就是加锁的核心,占8个字节
klass pointer 记录指向对象class文件的指针,占4个字节
instance data 对象变量数据
padding 对其数据,在64位版本虚拟机规范中要求对象大小必须是8的倍数,不足部分使用padding补齐
final Object monitor = new Object();  这个monitor 在内存中的大小是多少字节呢?答案是16个字节,8+4+0=12,12不能不8整除,所以补齐后的大小为16;
OFF  SZ   TYPE DESCRIPTION               VALUE0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)8   4        (object header: class)    0xf80001e512   4        (object alignment gap)    

如何查看对象在内存中的布局

我们可以通过JOL(Java Object Layout)来查看对象在内存中的布局;

       <dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.16</version></dependency>
public static void main(String[] args) {//JKD8延迟4S开启偏向锁Thread.sleep(5000);final Object monitor = new Object();System.out.println(ClassLayout.parseInstance(monitor).toPrintable());
}

这个代码的运行结果就是上文中的布局信息。

markword数据结构

在上文中提到markword是对象加锁的核心,那么这部分数据的结果是什么样子呢?下面就来介绍一下markword数据结构;这也一些大厂面试中经常会遇到的部分。

锁状态 56 bit 1bit 4bit 1bit 2bit
是否偏向锁 锁状态
无锁 unused 25bit hashcode 31bit unused 分代年龄 0 01
偏向锁 线程ID epoch 2bit unused 分代年龄 1 01
轻量级锁 指向战中锁记录的指针 00
重量级锁 指向互斥锁的指针 10
GC 11

加锁后发生了什么

聊完了JOL以后,我们来看看对象加锁后到底发生了什么?我们通过如下一段代码来看看:

 public static void main(String[] args) {final Object obj = new Object();System.out.println("启动后对象布局:\n" + ClassLayout.parseInstance(obj).toPrintable());//JKD8延迟4S开启偏向锁Thread.sleep(5000);//可偏向 101final Object monitor = new Object();System.out.println("延迟5秒后对象布局:\n" + ClassLayout.parseInstance(monitor).toPrintable());//偏向锁synchronized (monitor) {System.out.println("对象加锁后的布局:\n" + ClassLayout.parseInstance(monitor).toPrintable());}System.out.println("对象释放锁后的布局:\n" + ClassLayout.parseInstance(monitor).toPrintable());}

输出的结果是:

启动后对象布局:
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)8   4        (object header: class)    0xf80001e512   4        (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total延迟5秒后对象布局:
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)8   4        (object header: class)    0xf80001e512   4        (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total对象加锁后的布局:
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE0   8        (object header: mark)     0x00007fd89d00e805 (biased: 0x0000001ff627403a; epoch: 0; age: 0)8   4        (object header: class)    0xf80001e512   4        (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total对象释放锁后的布局:
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE0   8        (object header: mark)     0x00007fd89d00e805 (biased: 0x0000001ff627403a; epoch: 0; age: 0)8   4        (object header: class)    0xf80001e512   4        (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

从代码的执行结果可以知道在启动后是不支持偏向锁的,延迟4S后,对象是可偏向的(biasable)在此之后首次获取到的就是偏向锁(biased),释放锁以后可以发现markword并没有什么变化。

偏向锁

什么是偏向锁

偏向锁是HotSpot虚拟机中使用的一种锁优化技术,用于减少无竞争锁定的开销。它旨在避免在获取监视器锁时执行CAS操作。偏向锁是基于无竞争的假设下,即假设监视器一直归给定的线程所有,直到不同的线程尝试获取它。当改对象没有被其他线程访问过时,则初始锁定偏向于改线程,即将在markword 中记录改线程ID,表示改线程持有锁,后续改线程对该对象锁定时避免CAS操作,可以直接使用。

获取偏向锁的流程图如下:

偏向锁定时hashCode 哪去了?

通过上文介绍markword布局可以知道,偏向锁定时markword中保存的是偏向线程的ID,那么如果此时执行hashCode又是一个什么场景呢?下面我们通过执行代码看看。

public static void main(String[] args) throws Exception {//JKD8延迟4S开启偏向锁Thread.sleep(5000);//可偏向 101final Object monitor = new Object();System.out.println("延迟5秒后对象布局:\n" + ClassLayout.parseInstance(monitor).toPrintable());//偏向锁synchronized (monitor) {System.out.println("对象加锁后的布局:\n" + ClassLayout.parseInstance(monitor).toPrintable());}System.out.println("对象释放锁后的布局:\n" + ClassLayout.parseInstance(monitor).toPrintable());System.out.println(monitor.hashCode());System.out.println("执行hash后的对象布局:\n" + ClassLayout.parseInstance(monitor).toPrintable());
}

代码执行的结果如下图所示,可以发现执行hashCode()方法后,锁对象撤销了偏向锁,变为了无锁状态

延迟5秒后对象布局:
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)8   4        (object header: class)    0xf80001e512   4        (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total对象加锁后的布局:
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE0   8        (object header: mark)     0x00007fde3e009005 (biased: 0x0000001ff78f8024; epoch: 0; age: 0)8   4        (object header: class)    0xf80001e512   4        (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total对象释放锁后的布局:
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE0   8        (object header: mark)     0x00007fde3e009005 (biased: 0x0000001ff78f8024; epoch: 0; age: 0)8   4        (object header: class)    0xf80001e512   4        (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total1937962514
执行hash后的对象布局:
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE0   8        (object header: mark)     0x0000007382f61201 (hash: 0x7382f612; age: 0)8   4        (object header: class)    0xf80001e512   4        (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

为什么需要偏向锁

这个主要是因为我们很多代码中无意识地引入了同步锁,很多时候这些锁都是没有竞争的。如:

    public void println(String x) {synchronized (this) {print(x);newLine();}}

为什么从JDK15开始默认关闭偏向锁

通过上文了解,好像偏向锁可以极大提升锁性能;理想很美好,现实很残酷;在实际使用过程中性能提升远远没有预期那么明显, 在早并发场景下禁用偏向锁后反而性能更好。为什么会这样呢,这个有多方面原因导致的。

第一:早期JDK集合API都是同步的,如HashTable ,Vector 等,这些API在现在代码中很少能够看到,而代替的是HashMap, ArrayList;

第二:在无竞争场景下,获取锁的成本很低,但是一旦有了竞争,撤销锁却是一件昂贵的事。撤销偏向锁不是在有竞争的时候就可以立即撤销,而是需要等到安全点(safe point)才能撤销也就是撤销偏向锁需要STW。

什么是安全点

上文提到偏向锁需要等到安全点以后才能撤销,那么什么是安全点呢?安全点就是代码执行的一些特殊位置,当线程执行到这个位置时候表示线程是安全的。比较典型的就是GC处理时需要等待安全点才可以执行GC操作。在编译阶段编译器会在一些特殊的位置插入读取全局Safepoint Polling内存页,如果需要安全点被标记则Safepoing Pollling内存页不可读,使得当前现在阻塞在安全点。如果VM Thread发现还有线程still_running 则会自旋等待,直到所有线程都进入安全点,still_running  数量为0时才可以执行一些不安全操作。

JDK8 为什么要延迟4S后开启偏向锁

通过上文我们很容易理解,因为在JVM启动过程中必然会出现大量锁竞争,偏向锁因为锁竞争导致偏向锁撤销的操作成本又非常高,为了提升性能默认延迟4S后开启偏向锁。

锁升级流程

锁升级流程是 无锁->偏向锁->轻量级锁(thin lock)->重量级锁(fat lock); 下面我们通过代码来掩饰一些锁升级过程

    public static void main(String[] args) throws Exception {final Object obj = new Object();System.out.println("启动后对象布局:\n" + ClassLayout.parseInstance(obj).toPrintable());//JKD8延迟4S开启偏向锁Thread.sleep(5000);//可偏向 101final Object monitor = new Object();System.out.println("延迟5秒后对象布局:\n" + ClassLayout.parseInstance(monitor).toPrintable());//偏向锁synchronized (monitor) {System.out.println("对象加锁后的布局:\n" + ClassLayout.parseInstance(monitor).toPrintable());}System.out.println("对象释放锁后的布局:\n" + ClassLayout.parseInstance(monitor).toPrintable());System.out.println(monitor.hashCode());System.out.println("执行hash后的对象布局:\n" + ClassLayout.parseInstance(monitor).toPrintable());new Thread(() -> {synchronized (monitor) {System.out.println("线程2对象加锁后的布局:\n" + ClassLayout.parseInstance(monitor).toPrintable());}}).start();Thread.sleep(1000);synchronized (monitor) {new Thread(() -> {try {monitor.wait();} catch (Exception e) {e.printStackTrace();}}).start();System.out.println("重量级锁:\n"+ClassLayout.parseInstance(monitor).toPrintable());}}

结果如下:

启动后对象布局:
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)8   4        (object header: class)    0xf80001e512   4        (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total延迟5秒后对象布局:
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)8   4        (object header: class)    0xf80001e512   4        (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total对象加锁后的布局:
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE0   8        (object header: mark)     0x00007ff66980d805 (biased: 0x0000001ffd9a6036; epoch: 0; age: 0)8   4        (object header: class)    0xf80001e512   4        (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total对象释放锁后的布局:
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE0   8        (object header: mark)     0x00007ff66980d805 (biased: 0x0000001ffd9a6036; epoch: 0; age: 0)8   4        (object header: class)    0xf80001e512   4        (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total1937962514
执行hash后的对象布局:
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE0   8        (object header: mark)     0x0000007382f61201 (hash: 0x7382f612; age: 0)8   4        (object header: class)    0xf80001e512   4        (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total线程2对象加锁后的布局:
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE0   8        (object header: mark)     0x000070000f36c908 (thin lock: 0x000070000f36c908)8   4        (object header: class)    0xf80001e512   4        (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes totaljava.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE0   8        (object header: mark)     0x00007ff66b01733a (fat lock: 0x00007ff66b01733a)8   4        (object header: class)    0xf80001e512   4        (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

通过执行的结果可以发现锁的升级流程是从 偏向锁(biased)->轻量级锁(thin Lock)->重量级锁(Fat Lock)

轻量级锁(Thin Lock)

在轻量级锁中,获取锁的线程首先拷贝对象头中的markword到帧栈的锁记录中。拷贝成功后使用CAS更新markword 锁记录为当前线程的指针;如果更新成功就表示当前线程持有了改对象的锁,如果更新失败,就表示当前锁存在竞争,此时不是立即升级到重量级锁,而是继续尝试(自旋)获取轻量级锁,当失败多次后,就升级为重量级锁。为什么不继续自旋呢?因为自旋就意味着消耗CPU资源。

自旋锁

在轻量级锁中获取锁失败后会继续尝试获取锁,这个流程就称为自旋;如果锁的粒度很小持有锁的时间很短,通过轮询几次以后就可以获取锁这个成本相对重量级锁而言还是很低的。

使用-XX:-UseSpinning参数关闭自旋锁优化;-XX:PreBlockSpin参数修改默认的自旋次数。

自适应自旋锁

自旋意味着占用CPU资源,如果线程多,CPU资源比较少时,CPU资源全部被自旋占用,反而造成了CPU资源的浪费。那么如何设置自旋的次数呢?这个是一件很困难的事情。自适用自旋锁就是为了解决锁竞争的时间不确定性这个问题的。JVM自己去根据锁竞争的情况去优化自旋次数。

重量级锁(Fat Lock)

在重量级锁中获锁和释放锁的流程和轻量级锁差不多,主要的区别是,获取锁失败后当前线程会阻塞;释放锁以后会唤醒阻塞线程。那么为什么这个是重量级锁呢?主要是因为线程的阻塞和唤醒涉及到线程切换以及系统调用引起的用户态和内核态切换等,这些都是成本比较高的操作,所以说是重量级锁。

Synchronized 锁实现

针对上文中的代码编译后通过javap 查看字节码, 可以发现synchronized在字节码中是通过

monitorenter 进入同步代码, monitorexit 退出。

        95: monitorenter96: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;99: new           #7                  // class java/lang/StringBuilder102: dup103: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V106: ldc           #19                 // String 对象加锁后的布局:\n108: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;111: aload_2112: invokestatic  #11                 // Method org/openjdk/jol/info/ClassLayout.parseInstance:(Ljava/lang/Object;)Lorg/openjdk/jol/info/ClassLayout;115: invokevirtual #12                 // Method org/openjdk/jol/info/ClassLayout.toPrintable:()Ljava/lang/String;118: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;121: invokevirtual #13                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;124: invokevirtual #14                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V127: aload_3128: monitorexit129: goto          139132: astore        4134: aload_3135: monitorexit

监视器锁 (monitor)

_object 锁寄生的对象,锁资源不能凭空出现,必须寄生在一个对象上
_owner 拥有锁的线程
 _WaitSet 等待锁资源线程,自旋获取锁
_EntryList 未获取锁被阻塞的线程
  1. 一个线程竞争锁资源时,先放入entry set
  2. 锁资源释放后,从entry set中选取一个线程尝试占有锁
  3. 调用await 进入wait set
  4. 调用notify 后 尝试占有锁,如果占有锁失败,则放入entry set

总结

JVM内部锁升级过程(偏向锁,轻量级锁,重量级锁)相关推荐

  1. 谈谈JVM内部锁升级过程

    简介: 对象在内存中的内存布局是什么样的?如何描述synchronized和ReentrantLock的底层实现和重入的底层原理?为什么AQS底层是CAS+volatile?锁的四种状态和锁升级过程应 ...

  2. 锁升级过程(无锁、偏向锁、轻量级锁、重量级锁)

    文章目录 Synchronized锁升级的背景 Synchronized的性能变化 Java5之前,用户态和内核态之间的切换 为什么每个对象都可以称为一把锁? Java6开始优化Synchronize ...

  3. 从 class 文件 看 synchronize 锁膨胀过程(偏向锁 轻量级锁 自旋锁 重量级锁)

    大家好,我是烤鸭: 前几天看马士兵老师的并发的课,里边讲到了 synchronize 锁的膨胀过程,今天想用代码演示一下. 1.  简单介绍 关于synchronize jdk 1.5 以后的优化,由 ...

  4. 深入了解JVM锁升级过程

    对于java锁升级,很多人都停留在比较浅层的表面理解,一定程度下也许够用,但如果学习其中的细节,我们更好地理解多线程并发时各种疑难问题的应对方式! 因此我将锁升级过程中可能涉及的大部分细节或者疑问都整 ...

  5. 在c#中用mutex类实现线程的互斥_面试官经常问的synchronized实现原理和锁升级过程,你真的了解吗...

    本篇文章主要从字节码和JVM底层来分析synchronized实现原理和锁升级过程,其中涉及到了简单认识字节码.对象内部结构以及ObjectMonitor等知识点. 阅读本文之前,如果大家对synch ...

  6. synchronized锁升级过程及其实现原理

    本文链接:https://blog.csdn.net/wangyy130/article/details/106495180 问:为什么会有锁升级的过程呢 答:在java6以前synchronized ...

  7. synchronized 锁升级过程

    synchronized 锁升级过程就是其优化的核心:偏向锁 -> 轻量级锁 -> 重量级锁 class Test{private static final Object object = ...

  8. 存储过程没有执行完后没有释放锁_面试必问---synchronized实现原理及锁升级过程你懂吗?...

    synchronized实现原理及锁升级过程 前言: synchronized是Java内置的机制,是JVM层面的,而Lock则是接口,是JDK层面的 尽管最初synchronized的性能效率比较差 ...

  9. java synchronized关键字锁和锁类型、锁升级过程讲解

    概述 synchronized是java的一个关键字,用于对方法或者代码块添加一个同步锁,以实现操作的原子性,保证线程安全性,但是却会带来一些性能上的损耗. 这个关键字添加的是可重入锁,也就是同一个线 ...

最新文章

  1. 智慧健康,协同发展:清华大学携手天津市共同探索健康医疗大数据
  2. php扩展swoole安装,php 安装swoole扩展
  3. JS操作DOM元素属性和方法
  4. DeviceIoControl的使用说明
  5. Zookeeper的安装部署,zookeeper参数配置说明,集群搭建,查看集群状态
  6. 找出所有支持UI5的BSP application
  7. 阻尼衰减曲线用python_高阻尼隔震橡胶支座结构及防震效果
  8. 第25版 OpenStack Yoga 已发布
  9. nodejs实践录:简单的log日志模块
  10. android平台开发的安装
  11. fdisk、parted无损调整普通分区大小
  12. GridView(网格视图)+MotionEvent(触控事件)实现可以拖动排序的网格图
  13. 微信小程序——云开发入门
  14. 盲源分离(BSS)的学习总结(PCA、ICA)
  15. Net分布式系统之七:日志采集系统(1)
  16. 华为nova10和荣耀70哪个值得买 哪个性能更好
  17. 未来的世界是,方向比努力重要,能力比知识重要,健康比成绩重要,生活比文凭重要,情商比智商重要!
  18. 重磅!谷歌开源TensorFlow 3D场景理解库
  19. Golang学生管理系统(函数+结构体版)
  20. 了解电感—参数、特性、选型

热门文章

  1. 高中会考计算机试题及答案,高中计算机会考试题及答案
  2. HB100多普勒雷达+STM32L476VGTx测速系统的电路设计(滤波放大比较器)
  3. 判断一个字符串是否为“回文”
  4. Apk脱壳圣战之—如何脱掉“梆梆加固”的保护壳
  5. 袁永福软件行业从业经历
  6. ❤️❤️❤️Unity废柴看过来,手把手教你做植物大战僵尸(十二)—— 向日葵生产太阳
  7. 彩票预测应该用什么神经网络
  8. 提高网络营销的转化只需掌握这四步
  9. archivelog模式和flashback db以及guarantee restore point之间的相互制约关系!
  10. SA8155P QCOM 车载系统介绍