JUC:synchronized优化——锁的升级过程(偏向锁->轻量级锁->重量级锁)以及内部实现原理

锁的类型

重量级锁、轻量级锁、偏向锁。
加锁过程:偏向->轻量级->重量级

轻量级锁

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

轻量级锁对使用者是透明的,即语法仍然是 synchronized,锁的升级什么的不用我们考虑。

static final Object obj = new Object();

public static void method1() {
    synchronized( obj ) {
        // 同步块 A
        method2();
    }
}

public static void method2() {
    synchronized( obj ) {
        // 同步块 B
    }
}

每一个线程的栈帧中都会包含一个锁记录结构,记录对象头中的markword。

在这里插入图片描述

锁记录中Object reference指向锁对象,并尝试用 cas 交换 Object 的 Mark Word,将Mark Word的值存入锁记录中。

在这里插入图片描述

若cas成功,对象头中存储锁地址和锁状态00(轻量级),表示该线程给此对象加锁。

在这里插入图片描述

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

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

在这里插入图片描述

当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一。

在这里插入图片描述

当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头

  • 成功,则解锁成功。
  • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。

重量级锁

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时会进行锁膨胀,将轻量级锁膨胀为重量级锁。

static Object obj = new Object();
public static void method1() {
    synchronized( obj ) {
        // 同步块
    }
}

当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁。

在这里插入图片描述

这时Thread-1就会CAS失败,进入锁膨胀过程。

  • Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
  • 然后自己进入 Monitor 的 EntryList BLOCKED
  • Object 对象头锁标记变为10(重量级)

在这里插入图片描述

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

简单来说,我们可以简化一下:

  • 就是没竞争时,自己正常加锁运行解锁即可,同一线程有锁的重入,防止没必要的加锁解锁过程。
  • 如果有竞争时,新线程进入阻塞队列等待,当解锁时,唤醒阻塞队列中的线程。

自旋优化

重量级锁竞争的时候,可以使用自旋(不断的循环尝试获取重量级锁)来进行优化,如果当前线程自旋成功(当前持有锁的线程释放了锁),这时当前线程就可以避免阻塞。(阻塞再恢复,会进行上下文切换,耗费性能)

但是自旋也是会消耗性能的,尤其是进程非常多的时候,一堆进程在不断循环等待锁被释放,优点就是及时,一释放锁就能感应到。

偏向锁

在锁重入时,会产生锁记录,每次都需要尝试CAS,虽然会失败,但是CAS这个操作是没必要的。

优化:

将对象头中记录持有该锁的线程id,如果发生锁的重入,检测对象头,若是自己的,那么就不用再CAS了。

对比:

在这里插入图片描述

在这里插入图片描述

偏向锁的细节

在这里插入图片描述

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0。

  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟。

  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值。

  • 正常状态对象一开始是没有 hashCode 的,第一次调用才生成。

  • 调用了 hashCode() 后会撤销该对象的偏向锁。

从图中也可以看出,偏向锁的markword没有位置可以放hashcode了,除非转变为轻量级锁,所以调用对象的hashCode会撤销他的偏向锁。

偏向锁的撤销

除了上面说的调用hashCode,也可用 让其他线程使用偏向锁,这时偏向锁会升级为普通锁。

不过两个线程需要错开加锁,不然会升级为重量级锁。

批量重偏向

可用发现,就算错开没用竞争,那么锁也会从偏向锁转变为普通锁,那其实锁的转变也是消耗性能的,所以就有了偏向锁的重偏向这个功能。

如果对象被多个线程访问,但没有竞争(错开),这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID 为 T2线程。

当某对象(这一类的不同对象)撤销偏向锁阈值超过20次后,jvm会觉得,我是不是偏向错了,于是会在给这些对象加锁的时候重新偏向至新加锁线程。

批量撤销

当相同类型不同对象撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向,这个锁竞争太激烈了。于是整个类的所有对象都会变为不可偏向的,新建的该类型对象也是不可偏向的(轻量级)。

锁消除

简单来说,当jvm调用某方法达到一定阈值,就会用记时编译器优化,发现锁加和不加没有影响时,就会去除掉锁。

public class MyBenchmark {
    static int x = 0;
   
    public void a() throws Exception {
        x++;
    }
    
    public void b() throws Exception {
        Object o = new Object();
        synchronized (o) {
            x++;
        }
    }
}

比如Object为局部变量,根本不会逃离此方法作用范围,不能被共享,那加不加锁没啥意义,jvm就会去除掉锁去运行,从而优化性能。

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-03 23:58:02       106 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-03 23:58:02       116 阅读
  3. 在Django里面运行非项目文件

    2024-04-03 23:58:02       95 阅读
  4. Python语言-面向对象

    2024-04-03 23:58:02       103 阅读

热门阅读

  1. 财务管理 基础1:除了利润,一切都是扯淡

    2024-04-03 23:58:02       41 阅读
  2. GDAL源码剖析(十一)之OGR投影说明

    2024-04-03 23:58:02       35 阅读
  3. Go语言介绍及Go语言成功的项目列举

    2024-04-03 23:58:02       39 阅读
  4. 【前端浅谈】前端开发语言有哪些

    2024-04-03 23:58:02       39 阅读
  5. 每日一题 --- 有效的括号[力扣][Go]

    2024-04-03 23:58:02       46 阅读
  6. 集群式无人机仿真环境和数据集

    2024-04-03 23:58:02       43 阅读
  7. LeetCode-热题100:234. 回文链表

    2024-04-03 23:58:02       39 阅读
  8. C++(12): std::mutex及其高级变种的使用

    2024-04-03 23:58:02       40 阅读
  9. YOLO_Tracking 实践 (环境搭建 & 案例测试)

    2024-04-03 23:58:02       47 阅读