一、准备工作:
1、导入依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
2、了解Java对象结构
对象结构由对象头,对象实例数据和对齐填充组成,而本文主要是通过解析对象头的MarkWord来分析锁的状态以及膨胀过程,以下是Markword在64位操作系统下的结构图
二、正文
无锁:
无锁不可偏向:
- 由于对象的hash码需要在Markword里面占用31个比特,所以如果需要存储对象的hash码的话,那锁的状态会处于无锁不可偏向
- 未设置偏向延迟-XX:BiasedLockingStartupDelay=0,由于jvm默认启动4s后开启锁偏向机制,所以需要将偏向延迟设置为0。
在设置后
无锁可偏向:
- 无锁状态且避免了上述的无锁不可偏向状态
- 尚未有线程进行对象进行CAS操作,既将自己的线程ID写入Markword
偏向锁:
在没有实际竞争的情况下,还能够针对部分场景继续优化。如果不仅仅没有实际竞争,自始至终,使用锁的线程都只有一个,那么,维护轻量级锁都是浪费的,所以锁在偏向锁的状态下就可以满足以最低的性能损失换取线程安全。
如果对象的锁状态处于无锁可偏向状态,此时线程读取对象会进行CAS操作尝试将自己的线程ID写入Markword,如果CAS失败则表明有另外一个线程B抢先获得了偏向锁,此时则会撤销另一个线程B的偏向锁,等待全局安全点 JVM safepoint ( 此时间点, 没有线程在执行字节码) ,将线程B的锁升级为轻量级锁。当线程在执行完同步代码块以后, 并不会尝试将 MarkWord 中的 thread ID 赋回原值 。这样做的好处是: 如果该线程需要再次对这个对象加锁,而这个对象之前一直没有被其他线程尝试获取过锁,依旧停留在可偏向的状态下, 即可在不修改对象头的情况下,直接认为偏向成功。
如果对象的锁状态处于有锁已偏向,如果相等,则证明本线程已经获取到偏向锁,可以直接继续执行同步代码块
如果不等,则证明该对象目前偏向于其他线程,需要撤销偏向锁。
偏向锁的撤销并不是将锁状态恢复到无锁可偏向,而是等待全局安全点,将持有锁的线程升级为轻量级锁。
偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定。
轻量级锁:
当其他线程正在持有锁,当前线程会进入阻塞状态,重新唤起切换线程,需要执行ring0级别的CPU指令集(内核代码),执行内核代码要调用当前进程的内核栈,操作系统需要从用户态切换到核心态,这会占用处理器时间,自旋机制的目标是降低线程切换的开销,但是自旋机制也有弊端,如果另一条线程持有锁的时间太长,那这时候长时间自旋就是白白消耗CPU资源。在jdk6之后引入了自适应自旋,自旋的时间次数不再固定而是会根据锁上一次的自旋次数和锁持有线程的状态来决定,如果上次自旋10次没拿到锁,那么下次可能自旋8次,如果上次自旋了10次拿到了锁,下次可能自旋12。
如果锁竞争激烈,我们不得不依赖于重量级锁,让竞争失败的线程阻塞;如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。
顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。
当然,由于轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后再膨胀为重量级锁。