锁、锁、锁看了synchronized源码,请别和我说它慢JDK15之后再也不用担心了

本文详细介绍了Java中synchronized的锁优化机制,包括偏向锁、轻量级锁和重量级锁的工作原理。通过分析JVM源码,揭示了从HotSpot的模板解释器到不同CPU架构下的实现。文中指出,JDK15之后将废弃偏向锁,但目前大多数应用仍在使用JDK8。同时,文章提供了关于Java线程和锁的面试知识点,包括内存布局和锁的升级过程。
摘要由CSDN通过智能技术生成

我们都知道 Java 之所以可以一次编译到处运行,完全是因为字节码的原因,字节码就相当于中间层屏蔽了底层细节。但是想要在机器执行,最终还是要翻译成机器指令。

而 JVM 是通过 C/C++ 来编写的。Java 程序编译后,会产生很多字节码指令,每一个字节码指令在 JVM 底层执行的时候又会编程一堆 C 代码,这一堆 C 代码在编译之后又会编程很多的机器指令。这样我们的 Java 代码到最终执行的机器指令那一层,所产生的机器指令时指数级的。这也就导致了 Java 执行效率低下。
2020整理收集的一线互联网公司面试真题(都整理成文档),有很多干货,包含netty,spring,线程,spring cloud等详细讲解,也有详细的学习规划图,面试题整理等,我感觉在面试这块讲的非常清楚:获取面试资料只需:点击这里领取!!!暗号:CSDN在这里插入图片描述

早期的 JVM 是因为解释执行慢而被人诟病,那么有没有办法优化这个问题呢?我们发现,之所以慢是因为 Java 和机器指令之间隔了一层 C/C++,而 GCC 之类的编译器又不能做到绝对的智能编译,其产生的机器码效率就不是很高。因此我们只要跳过 C/C++ 这个层次,直接将 Java 字节码和本地机器码进行一个对应就可以了。

因此 HotSpot 的工程师们废弃了早期的解释执行器,而采用了模板执行器。所谓的模板就是将一个 Java 字节码通过人工手动的方式编写为固定模式的机器指令,这部分不在需要 GCC 的帮助。这样就可以大大减少最终需要执行的机器指令,所以才能提高效率。

在 OpenJDK12 源码中,JVM 所有的解释器都在 src/hotspot/share/interpreter 目录下,templateInterpreter.cpp 就是模板解释器的代码位置。分析这里的 initialize 方法,我们可以在 templateTable.cpp 中找到和 synchronized 相关的两个指令(monitorenter、monitorexit)的实现方式。当然这里面还有其他我们熟悉的指令,比如 invokedynamic、newarray 等指令。

def(Bytecodes::_monitorenter, ____|disp|clvm|____, atos, vtos, monitorenter, _);
def(Bytecodes::_monitorexit, ____|____|clvm|____, atos, vtos, monitorexit, _ );

monitorenter 执行逻辑

这里倒数第二个参数的 monitorenter 函数和 monitorexit 函数是对应字节码的机器码模板的位置。这里我们看下 monitorenter 的实现。因为机器码的实现和 CPU 相关,这里我们看下 x86 的实现(templateTable_x86.cpp)。当然也可以在 src/hotspot/cpu 下看到其他的实现,比如 PPC、ARM、S390等。

void TemplateTable::monitorenter() {
   
  ...
    // 将要锁的对象指针放到 BasicObjectLock 的 obj 变量中
    __ movptr(Address(rmon, BasicObjectLock::obj_offset_in_bytes()), rax);
    // 跳转执行 lock_object 函数
    __ lock_object(rmon);
  ...
  }

void InterpreterMacroAssembler::lock_object(Register lock_reg) {
   

  // 如果使用重量级锁,则直接进入 monitorenter() 执行
  if (UseHeavyMonitors) {
   
    call_VM(noreg,
            CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
            lock_reg);
  } else {
   
    ...

    // 对象指针存入 obj_reg
    movptr(obj_reg, Address(lock_reg, obj_offset));

    // 关于偏向锁的处理
    if (UseBiasedLocking) {
   
      // lock_reg : 存储指向 BasicObjectLock 的指针
      // obj_reg : 存储锁对象的指针
      // slow_case : 标记,类似于 goto, 这里指的是 InterpreterRuntime::monitorenter()
      // done: 标记,标志着获取锁成功。
      // slow_case 和 done 也被传入,这样在 biased_locking_enter() 中,就可以根据情况跳到这两处了。
      biased_locking_enter(lock_reg, obj_reg, swap_reg, tmp_reg, false, done, &slow_case);
    }
    ...

   // slow_case逻辑,需要进入 InterpreterRuntime::monitorenter() 中获取锁。
    bind(slow_case);
    // 为 slow_case 调用运行时方法
    call_VM(noreg,
            CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
            lock_reg);


    // 这里的 done 和上面传入到偏向锁的 done 是一样的。直接跳到这表明获取锁成功,接下来就会返回进行字节码的执行了。
    bind(done);
  }
}

从代码可以看出,如果启用了重量级锁,那么就直接走重量级锁的逻辑(monitorenter);不然会先处理偏向锁的逻辑,然后不满足会再回到 monitorenter 中。

偏向锁:-XX:+UseBiasedLocking,
JDK1.6 之后默认启用 重量级锁:-XX:+UseHeavyMonitors

偏向锁、轻量级锁以及重量级锁

我们提到了重量级锁和偏向锁,这两个是什么意思呢?

我们都知道 Java 的线程是映射到操作系统的原生线程之上的。无论是是阻塞还是唤醒一个线程,都需要操作系统的帮助,这就需要从用户态转换到核心态中。而很多人说 synchronized 慢也正是由于这个原因。之前的文章也说过 synchronized 实际上是通过操作系统的互斥量来实现的,而这也被称为重量级锁。

相对于重量级锁,还有一个叫做轻量级锁。它的加锁不是通过操作系统来实现的,而是通过 CAS 配合 Mark Word 一起实现的,后面我会通过源码来展示它的实现方式。

而偏向锁相对于轻量级锁更加轻量,这里的偏向指的是偏向某一个线程。如果只有一个线程来获取锁,那么锁对象就会偏向这个线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。

接下来,我们沿着源码从 偏向锁 -> 轻量级锁 -> 重量级锁 这样来分析 JVM 是如何进行优化的。

内存布局

锁状态转化及对象 Mark Word 的关系

实际上,锁的优化逻辑在 JDK 中的 Wiki 中已经有一个提纲挈领的图了。这里我先贴出来,后面的代码分析也会跟着这张图走。在这里插入图片描述

偏向锁

偏向锁的启动

偏向锁会在虚拟机启动后的4秒之后才会生效,我们可以从 hotspot/share/runtime/biasedLocking.cpp 看到这样的设定。

void BiasedLocking::init() {
   
  if (UseBiasedLocking) {
   
    if (BiasedLockingStartupDelay > 0) {
   
      EnableBiasedLockingTask* task = new EnableBiasedLockingTask(BiasedLockingStartupDelay);
      task->enroll();
    } else {
   
      VM_EnableBiasedLocking op(false);
      VMThread::execute(&op);
    }
  }
}

// 上面的 task 最终会调用这个方法,将锁对象的类的 mark word 的后三位设置为 101
static void enable_biased_locking(Ins
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值