Java对象头与锁的升级

概述

这篇文章主要用来总结一下Java对象头的Mark Word 与Java 锁升级的过程。(我们这里用32bit 的虚拟机为例)

Java 对象头

Java 对象包含三个部分

  • 1 Java 对象头
  • 2 元数据(包括类的属性名等)
  • 3 字节填充(对象的大小必须是8bit 的倍数)

Mark Word

Java 的对象头包含一下三个部分

  • 1 Mark Word(32bit)
  • 2 指向类的指针(32bit, 指向存放在方法区的Class 对象)
  • 3 数组长度(只有当对象为数组的时候,才有这一部分数据 32bit)

Mark Word记录了对象运行时的数据,包括HashCode, GC分带年龄, 是否持有偏向锁, 还有锁标志等。
如下图:
IMAGE

锁状态与锁的升级

如上图所示,Java对象的锁状态一共有四种,级别从低到高依次为: 无锁(01) -> 偏向锁(01) -> 轻量级锁(00) -> 重量级锁(10).要注意的锁是可以升级的,锁的升级目的是为了提高锁的获取效率和释放效率。下面我们来说一下每一个锁状态的意义。

偏向锁

HotSpot 研究员发现很多时候,锁不存在多线程之间的竞争,而是总是有同一个线程获取。如果在对象头加入上一次成功获取锁的线程Id,等待下次这个线程再次获取锁的时候(此时无其他线程竞争锁),那么线程不在需要通过自旋来获取锁,而是通过简单的测试对象记录的线程Id是否与当前线程的Id相等,如果相等直接加锁成功(线程Id相当于一个缓存的作用。),当然解锁也是如此。

当然以上只是偏向锁的一种使用场景,关于场景我们分三种情况来讨论:

  • 1.只有一个线程T1 获取锁(上述场景)
  • 2.线程T1与线程T2交替获取锁
  • 3.线程T1与线程T2同时进入同步块,竞争锁。

当出现第二种情况: 两个线程交替获取锁,那么线程T1成功获取锁以后,线程T2尝试竞争锁的时候,他会检测线程T1是否还存活。如果下面层T1已经挂了,那么对象头会设置成无锁状态,并在T2成功获取锁后,重新偏向于线程T2.

当出现第三种情况: 两个线程同时竞争锁,当T2自旋获取锁失败时,表示存在锁竞争,当到达全局安全点的时候(会有资源消耗),偏向锁会被撤销,锁升级为轻量级锁。

轻量级锁

1 轻量级锁加锁
轻量级锁的加锁,在线程执行同步块之前,会现在当前线程的栈帧中创建存储Mark Word的锁记录里空间(官方称为Displace Mark Word),然后利用CAS 将对象头的MarkWord替换为指向锁记录的地址,如果成功,则获取锁成功,如果失败,则当前线程利用自旋来获取锁,若自旋到一定程度之后依然没有获取到锁,则锁会膨胀成重量级锁。

2 轻量级锁解锁
轻量级锁的解锁,在执行完同步块之后,会将Displace Mark Word里面的锁记录替换回对象头中。如果替换成功,则解锁成功,如果替换失败,则解锁失败,锁就会膨胀成重量级锁。

重量级锁

例如我们最经常看见的synchronized就是非常典型的重量级锁,通过指令moniter enter 加锁,moniter exit解锁。重量级锁的同步成本非常高,包括内核态与用户态的切换造成的资源损耗等。

对比与总结

优点 缺点 应用场景
偏向锁 加锁与解锁基本不消耗资源 如果存在线程竞争则撤销锁需要额外的消耗 只有一个线程访问同步块的情景
轻量级锁 竞争锁不需要线程切换,提供了执行效率 如果存在大量线程竞争锁,自旋会消耗CPU资源 适用于少量线程访问同步块,追求访问同步块的速度
重量级锁 线程不需要自旋,不会消耗过多cpu资源 线程切换需要消耗大量资源,线程阻塞,执行缓慢 同步块执行时间较长的情况。
如果看的爽,不如请我吃根辣条?