(4.6)轻量级锁(锁膨胀、锁自选、偏向锁、锁消除)

4.6 轻量级锁、偏向锁——Monitor升级

JDK6之前的加锁方式是:关联锁对象到Monitor进行加锁,Monitor是由操作系统提供的,加锁代价高。

JDK6之后,对加锁方式进行了优化,引入了轻量级锁、偏向锁等。

1. 轻量级锁

如果一个对象虽然有多个线程要加锁,但是加锁的时间是错开的(没有竞争),可以使用轻量级锁来优化。

如果这时有其他线程来获取轻量级锁,则被阻塞,加锁线程将轻量级锁升级为Monitor锁

假设有两个方法同步块,利用同一个对象加锁

private static final Object lock = new Object();

public static void method1() {
        synchronized (lock) {
            log.info("method1....");
            method2();
        }
}

private static void method2() {
        synchronized (lock) {
            log.info("method2...");
        }
}
  • synchronized加锁时,在栈帧中创建锁记录(Lock Record)。 锁记录的组成:

    • 锁对象指针(Lock Record address):锁对象指针记录锁对象的地址
    • 锁记录地址(Object reference):锁记录地址记录锁对象的Markword。
  • Object reference指向锁对象;然后尝试用锁记录地址CAS替换锁对象的MarkWord。

  • 如果成功,则MarkWord的值存入锁记录中,对象头的MarkWord存储了锁记录地址和状态00

  • 如果失败,则说明

    • 对象锁已经持有了其他线程的轻量级锁(对象头的状态为00),这时表示有竞争,进入锁膨胀状态
    • 对象锁持有了自己的锁(锁对象状态为00, 并且存储的锁记录地址是自己的锁记录的地址),表示执行了synchronized锁重入,则在栈帧中在创建一条锁地址为null的LockRecord,用于重入计数

  • 解锁时,是null的锁记录,表示有重入,这时清除锁记录,同时重入计数-1

  • 解锁时,是不为null的锁记录,则尝试cas恢复MarkWord给锁对象

    • 成功,解锁成功
    • 失败,则说明有竞争,轻量级锁已经升为重量级锁,进入重量级锁解锁流程
2. 锁膨胀

线程尝试CAS加轻量级锁时,失败,如果情况是有其他线程已经对锁对象加上了轻量级锁,则进行锁膨胀,将轻量级锁变为重量级锁。

  • 线程1尝试CAS加锁时,线程0已经对该对象加了轻量级锁

  • 进入锁膨胀。为锁对象申请Monitor锁,让锁对象的MarkWord指向Monitor,状态变为10; Monitor的Own指向线程0;自己进入EntryList进入阻塞状态

  • 线程0解锁时,尝试CAS将MarkWord恢复给锁对象,失败。说明已经锁膨胀。通过锁对象的MarkWord找到Monitor地址;将Owner设为null;唤醒EntryL中的阻塞线程,让阻塞线程获取重量级锁;Monitor记录线程的Hashcode。
  • 线程1解锁时,没有阻塞线程,将锁Hashcode age bias 01恢复给锁对象,解锁。
3. 锁自旋

重量级锁竞争时,竞争线程进入EntryList阻塞前,还可以使用自旋来优化,如果自旋成功,就避免了阻塞带来的上下文切换。

JDK6的自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会 高,就多自旋几次;反之,就少自旋甚至不自旋。

JDK7之后无法控制是否开启自旋锁。

优点:

  • 避免阻塞带来的上下文切换

缺点:

  • 自旋会占用CPU时间,多核自旋才能发挥优势
4. 偏向锁

当轻量级锁(没有竞争,就自己这个线程)+ 锁重入时,每次都需要生成锁记录,并尝试CAS替换对象头的MarkWord操作。

JDK6引入偏向锁做进一步优化: 只有第一次加锁时,不需要生成锁记录,将线程ID设置到锁对象的MarkWord中;之后锁重入时,检查MarkWord是否是自己的线程ID,只要不发生竞争,则可以一直使用偏向锁。

优点:避免了每次加锁都需要生成锁记录,并CAS的过程。

5. 撤销偏向锁

(1)调用hashcode

调用hashcode会撤销偏向锁,因为偏向锁的锁对象MarkWord存储的是线程ID,调用偏向锁会导致偏向锁被撤销。

  • 轻量级锁在锁记录中存储hashcode
  • 重量级锁在Monitor中记录hashcode

(2) 其他线程交错使用锁对象

会将偏向锁升级为轻量级锁

(3) 其他线程竞争使用锁对象

将偏向锁升级为重量级锁

(4) 调用wait/notify

wait、notify是重量级锁才有的机制,调用它们会将偏向锁升级为重量级锁

6. 批量重偏向

锁对象偏向线程1,这时有线程2交替获取锁,会撤销包含线程1ID的偏向锁(线程ID变为线程2ID,释放锁后,偏向锁ID恢复为线程1ID);

当阈值超过20此后,会在加锁时重新偏向线程2(即将锁对象的线程ID替换为线程2的ID)

7. 批量撤销

当撤销偏向锁次数超过40,JVM意识到重偏向也不对,于是整个类的所有对象都变为不可偏向锁,新创建的对象也是不可偏向锁。

8. 锁消除

如果锁对象是局部的、不同享的、没有竞争的,则JIT即时编译器会对代码进行优化,执行时不加锁。提高运行效率。

锁消除默认开启,关闭锁消除:

java -XX:-EliminateLocks -java HelloConcurrent.jar

手动开启锁消除

java -XX:EliminateLock -java HelloConcurrent.jar
posted @ 2022-08-24 16:22  言思宁  阅读(348)  评论(0编辑  收藏  举报