Java中的偏向锁、轻量级锁、重量级锁以及锁升级

什么是锁?

Java中的锁的状态有四种:无锁、偏向锁、轻量级锁、重量级锁,状态会随竞争状态改变,低等级的锁可以升级为高等级的锁,即锁升级;但反之不行,没有锁降级。
要理解锁升级,首先要了解一些基本概念:
加锁行为到底锁的是什么?
加锁记录存在哪里?
我们以Java最常见的synchronized为例

首先要理解,锁是一种排他状态,是一种竞争性的共享资源,谁拿到了这个共享资源,谁就持有了锁
如基于redis的分布式锁,谁成功设置了“锁”这个key,就代表了谁获取到了锁
反应到JVM中道理一样,Java中每个对象都可以作为锁,多个线程共同竞争同一个对象,谁竞争到了,谁就持有了该对象的锁
那么,如果竞争对象?
在JVM中,每个对象都对应着一个monitor,即互斥量。JVM通过字节码指令monitorenter和monitorexit来实现持有和释放monitor,持有了monitor即代表获取了对象的锁。编译过程会自动在同步代码块的头尾增加这两个指令

对象头

对象头是用来存储一些对象的额外数据的,这些数据与对象自身定义无关,因此为了节省空间,对象头里的内容是会动态改变的,在不同的情况下存储的内容不同。
对象头中的MarkWord记录着对象的锁状态,本文讲的锁升级的实现是要基于这个MarkWord的,其内容如下表所示:

存储内容 是否偏向 锁标志位 标志位对应状态说明
对象哈希码、对象分代年龄 0 01 无锁
指向栈中锁记录的指针 0 00 轻量级锁
指向互斥量(重量级锁)的指针 0 10 重量级锁
0 11 GC标记,要被回收
偏向线程ID、偏向时间戳、对象分代年龄 1 01 偏向锁

根据锁竞争的情况,锁状态不同,对象头MarkWord中的内容也会不同。

MarkWord与锁的关系

加锁

当一个线程在获取锁时,会首先在自己的栈帧中开辟一块空间叫做LockRecord,然后将锁对象的MarkWord拷贝到这个LockRecord中,然后线程使用CAS修改对象MarkWord中的内容,如轻量级锁下会将其修改为线程自己的指针记录;如果修改成功,则成功持有锁;如果修改失败,说明当前有其他线程在一同进行锁的获取,发生了竞争且当前线程竞争失败

释放锁

线程将自己栈帧中的LockRecord复制回对象MarkWord中,如果成功,本次同步完成;如果失败,说明当前线程持有锁的过程中有其他线程进行了锁竞争,锁升级为了重量级锁,这时候当前线程释放锁的同时需要唤醒其他竞争线程

下面就例子来详细描述过程

锁升级例子

我们现在假设有T1和T2两个线程进入代码同步块,锁对象为lock对象

  1. lock对象的MarkWord此时为:
    lockHashCode | age | 0 | 01 即无锁状态
  2. T1,T2同时访问同步代码块,两个线程均在自己的栈中创建LockRecord并复制MarkWord
  3. T1使用CAS成功将MarkWord的内容修改成T1的指针地址,并将锁标志位置为00,即轻量级锁,此时MarkWord为T1 address | 00
  4. T2使用CAS修改MarkWord时,发现当前MarkWord中的内容为T1 address | 00,与预期值lockHashCode | age | 0 | 01 不符,CAS失败;T2与T1在竞争锁时失败,T2进入自旋状态等待锁;
  5. T2自旋结束,再去访问MarkWord,发现锁标志仍为00,即锁仍由其他线程持有中,此时T2使用CAS将MarkWord修改为monitor address | 10,此时锁升级为重量级锁。同时T2进入阻塞状态,进入等待队列。
  6. 此时有其他线程T3来竞争锁,会直接被阻塞
  7. T1将要释放锁,使用CAS将LockRecord复制回MarkWord,但发现当前MarkWord值为monitor address | 10与预期值T1 address | 00不符合,CAS失败;此时T1会将monitor address | 10修改为0 | 10释放锁并唤醒阻塞等待的线程
  8. T2被唤醒后重新进行锁的争夺
  9. T2与T3再竞争时,谁CAS将MarkWord由0 | 10替换为monitor address | 10,谁就持有锁,另一方会陷入阻塞状态;当释放锁时会修改回0 | 10释放锁,并唤醒阻塞等待的线程

要注意锁只能升级不能降级,因此当在第5步T2将锁升级为重量级锁后,lock对象的锁将永远处于重量级锁状态,不会再回到轻量级锁状态。

自旋锁

自旋锁是在获取锁失败时不立刻进入阻塞,而是仍持有CPU时间做一会无用功,再尝试获取锁。
自旋锁仅存在于轻量级锁的周期中,与轻量级锁共生。当可以通过自旋获取到锁,锁不会升级,之后其他线程来获取锁也存在自旋状态;一旦锁升级为了重量级锁,自旋将不在,线程获取锁失败时会直接进入阻塞状态。这也是为什么分轻量级锁和重量级锁的原因,阻塞的分量还是很重的。

偏向锁

获取

偏向锁是什么?如果一个同步块只会被一个线程访问,每次进出代码块的加锁释放锁就是在浪费无用功,因此有了偏向的概念。
偏向锁开启的情况下,对象的MarkWord内容为0 | 0 | 0 | 1 | 10(无锁状态为0 | 0 | 0 | 01
线程T1在获取对象的锁时,会先尝试获取偏向锁,对象的MarkWord会被T1修改为T1 ID | Epoch | age | 1 | 01,即偏向锁状态,偏向T1,T1下次进入同步块中,只需要检查一下MarkWord中是否为指向自身线程的偏向锁即可,如果是,则之后的操作将不使用任何同步操作。

如果T2进入同步代码块,发现对象的MarkWord的偏向线程不是自己,会尝试修改偏向为自己,即先判断偏向锁指定的线程是否还活着,如果没活则直接重偏向为自己;如果还活着将在系统安全点safepoint暂停T1线程,将锁标记为轻量级锁模式,并更新MarkWord指向T1的一条帧栈记录,偏向锁模式结束。T1释放锁时发现MarkWord不再为偏向模式且指针记录指向自己的帧栈记录,会知道存在了锁竞争升级为了轻量级锁,释放锁的同时会清除这条指针记录

释放

轻量级锁的释放需要将LockRecord复制回对象头,偏向锁不同,直接释放掉自己栈中的LockRecord即可

发布了98 篇原创文章 · 获赞 9 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Mutou_ren/article/details/103538622