【一文读懂】 Java并发 - 锁升级原理

要明白锁的原理,首先要知道对象头

Java对象头

在Java中,一个对象一般由两部分组成 :1、对象头 ; 2、对象的成员变量信息

在32位的虚拟机中:

(1)普通对象的对象头长度64bit(8字节): 其中的32bit是Mark Word,另外32位是Klass Word,如下:

| Mark Word (32 bits) | Klass Word (32 bits) |

(2)数组对象的对象头长度96bit(12自己): 除了Mark Word和Klass Word,还有32bit的 array length,如下:

| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |

什么是 Mark Word 什么是 Klass Word 什么是array length ?

  • array length - 很明显这是标明数组长度的字段
  • Klass Word - 对象类型指针,通过该字段标明当前对象所属的类
  • Mark Word - 下面着重介绍

在不同状态下,Mark Word 会有不同的表现形式,对应关系如下:

Normal 就是对象的正常状态,以该状态为例

  • 25位是对象的hashcode
  • 4位是age,在垃圾回收中的分代年龄
  • 1位biased_lock,标明是否是对象锁
  • 2位(01),表明当前的加锁状态

Biased 是偏向锁状态 Lightweight Locked 是轻量级锁状态 Heavyweight Locked 是重量级锁状态 Marked for GC 是在JVM进行GC时的状态

无论是偏向锁、轻量级锁、重量级锁,都是在对象头中的Mark Word做文章

锁升级

在默认情况下,synchronized 会首先使用偏向锁,当发现存在竞争时,就会升级为轻量级锁,当竞争比较激烈时,轻量级锁就会升级为重量级锁 但是我们可以通过添加 VM 参数 -XX:-UseBiasedLocking 手动关闭偏向锁,那么synchronized 会直接进入轻量级锁

为什么会有锁升级?

因为从偏向锁 - 轻量级锁 - 重量级锁,互斥程度会越来越高,但是性能代价也越来越大,所以在一开始,如果资源竞争不是很激烈,应该优先选择程度较轻的锁,以下是《Java并发编程的艺术》一书中的原话

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在 Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状 态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏 向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高 获得锁和释放锁的效率,下文会详细分析。

重量级锁原理

操作系统会提供 Monitor 对象,对某个对象加上重量级锁之后,该对象头中的 Mark Word 中就被设置成指向 Monitor 对象的指针 即上图所示的 ptr_to_heavyweight_monitor:30

在Monitor对象中会有两个属性: 1、Owner ;2、EntryList Owner - 指当前这个对象的锁是由哪个对象持有的 EntryList - 是一个线程队列,当其他线程来竞争该对象锁失败时,就会进入EntryList 中阻塞

因为重量级锁的加锁过程需要使用到操作系统提供的 Monitor 对象,换言之,属于内核级别,因此性能代价是较大的,如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化

重量级锁的自选优化

重量级锁竞争的时候,可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),当前线程就可以避免阻塞

轻量级锁

每个线程的每个栈帧都会包含一个锁记录的结构

锁记录包含两个部分,其中一个部分用存储上锁对象的 Mark Word,另外一部分是对象指针 Object reference,用于指向上锁对象的地址

上锁过程如下,让锁记录中 Object reference 指向上锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录

如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,结果如下

如果cas 失败,有两种情况:

  • 其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
  • 自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数,如下:

解锁时有如下两种情况

  • 锁记录的值为 null,表示有重入,这时重置锁记录,表示重入计数减一
  • 锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头,如果失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀过程

假设此时Thread-0已经对某个对象上了轻量级锁,现实Thread-1也尝试对改对象加锁,必然加锁失败,进入锁升级

  1. 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
  2. 然后自己进入 Monitor 的 EntryList 阻塞

如下图

当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 Entry List 中 的Thread-1 线程

轻量级锁缺点

在很多情况下,一个对象在某段时间内,会不停被同一个线程上锁,即不断发生锁重入,这就会频繁的进行Mark Word的cas操作,这也是存在一定性能损耗的,因此诞生了偏向锁来优化

偏向锁

只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

比较Normal状态和偏向锁状态的Mark Word:

我们发现Normal状态下 biased_lock 是0,偏向锁的 biased_lock 是1; biased_lock 是偏向锁的开启标志,0表示禁用,1表示开启

而且对于Normal,用了 31bit 去存储对象的hashcode; 而偏向锁状态下不在记录对象的hashcode,而是记录线程编号:初始状态下 54bit 的thread标记都是0,当有线程对该对象上轻量级锁后, 54bit 的thread标记记录下当前线程编号,而且那么线程出了临界区释放了锁,thread不会发生改变,所以当该线程对该对象进行锁重入或者重新上锁时,发现里面的线程编号就是自己,就表示没有竞争,不用 CAS

而如果有另外一个线程要对该对象上锁时,发现当前记录着其他线程的编号,就进行锁膨胀,升级为轻量级锁

end

评论

新增邮件回复功能,回复将会通过邮件形式提醒,请填写有效的邮件!