Java锁


在Java 6 及其以后,一个对象其实有四种锁状态,它们级别由低到高依次是:

  1. 无锁状态

  2. 偏向锁状态

  3. 轻量级锁状态

  4. 重量级锁状态

几种锁会随着竞争情况逐渐升级,锁的升级很容易发生,但是锁降级发生的条件会比较苛刻,锁降级发生在Stop The World期间,当JVM进入安全点的时候,会检查是否有闲置的锁,然后进行降级。

每个Java对象都有对象头。如果是非数组类型,则用2个字宽来存储对象头,如果是数组,则会用3个字宽来存储对象头。在32位处理器中,一个字宽是32位;在64位虚拟机中,一个字宽是64位。 对象头的内容如下表:

长度
内容
说明

32/64bit

Mark Word

存储对象的hashCode或锁信息等

32/64bit

Class Metadata Address

存储到对象类型数据的指针

32/64bit

Array length

数组的长度(如果是数组)

我们主要来看看Mark Word的格式:

锁状态
29 bit 或 61 bit
1 bit 是否是偏向锁
2 bit 锁标志位

无锁

0

01

偏向锁

线程ID

1

01

轻量级锁

指向栈中锁记录的指针

此时这一位不用于标识偏向锁

00

重量级锁

指向互斥量(重量级锁)的指针

此时这一位不用于标识偏向锁

10

GC标记

此时这一位不用于标识偏向锁

11

可以看到:

  1. 当对象状态为偏向锁时,Mark Word存储的是偏向的线程ID;

  2. 当状态为轻量级锁时,Mark Word存储的是指向线程栈中Lock Record的指针;

  3. 当状态为重量级锁时,Mark Word为指向堆中的monitor对象的指针。

总结锁的升级流程: 每一个线程在准备获取共享资源时:

  • 第一步,检查MarkWord里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁” 。

  • 第二步,如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。

  • 第三步,两个线程都把锁对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作, 把锁对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord。

  • 第四步,第三步中成功执行CAS的获得资源,失败的则进入自旋 。

  • 第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败 。

  • 第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。

各种锁的优缺点对比 [下表来自《Java并发编程的艺术》]:

优点
缺点
适用场景

偏向锁

加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距

如果线程间存在锁竞争,会带来额外的锁撤销的消耗。

适用于只有一个线程访问同步块场景

轻量级锁

竞争的线程不会阻塞,提高了程序的响应速度。

如果始终得不到锁竞争的线程使用自旋会消耗CPU

追求响应时间. 同步块执行速度非常快

重量级锁

线程竞争不使用自旋,不会消耗CPU。

线程阻塞,响应时间缓慢。

追求吞吐量. 同步块执行时间较长

Last updated