Synchronized底层原理系列之Synchronized的偏向锁实现原理

  • 阿里云国际版折扣https://www.yundadi.com

  • 阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
    作者简介
    专注于研究Linux内核、Hotspot虚拟机、汇编语言、JDK源码、各大中间件源码等等
    喜欢的话可以三连+关注~

    上篇文章已经对Synchronized关键字做了初步的介绍从字节码层面介绍了Synchronized关键字最终字节码层面就是monitorentermonitorexit字节码指令。并且拿Synchronized关键字和Java的JUC包下的ReentrantLock做了比较。Synchronized关键字的初体验-超链接地址

    那么本篇文章将开始深入解析Synchronized关键字的底层原理也就是解析Hotspot虚拟机对monitorentermonitorexit字节码指令的实现原理。

    理论知识

    相信各位读者在准备面试中都会背到关于Synchronized关键字的面试题什么对象头、锁标志位、偏向锁、轻量级锁、重量级锁锁升级的过程等等面试题。而对于一些不仅仅只想漂浮于表面的读者来说去看Synchronized底层源码只能说是一头雾水。所以笔者有考虑这方面所以理论知识给临时抱佛脚背理论的读者和底层源码给喜欢研究底层源码的读者都会在这个系列中。

    偏向锁存在的意义

    先从字面意思来解释偏向于某个线程是不是可以理解为偏向的这个线程获取锁都很效率呢那么为什么要存在偏向锁呢读者需要明白任何框架存在的意义不仅仅是为了某一部分场景肯定需要适配大部分场景而Synchronized关键字使用的场景可能并发高可能并发低可能几乎不存在并发所以实现者需要帮用户去适配不同的场景达到效率最高化。而对于几乎不存在并发的场景是不是可以理解为几乎只有一个线程拿到Synchronized锁所以就存在偏向锁去优化这种场景不让所有场景都去走很复杂的逻辑。

    偏向锁实现的流程

    1. 拿到锁竞争对象

    1. 从当前线程栈中获取到一个没有使用的BasicObjectLock用于记录锁状态

    1. 查看当前是否开启了偏向锁模式

    1. 查看当前偏向锁是否偏向的是当前线程如果偏向的是当前线程直接退出可以理解成命中缓存

    1. 查看当前是否已经锁升级了并且尝试撤销偏向锁想象一下并发过程中可能其他线程已经完成了锁对象的锁升级

    1. 当前epoch是否发生了改变如果发生了改变当前线程可以尝试获取偏向锁尝试成功直接退出

    1. 当前是否是匿名偏向或者已经偏向于某个线程但是不是当前线程此时可以尝试获取锁获取成功直接退出

    1. 如果不支持偏向锁或者第5步的撤销偏向锁失败了此时尝试膨胀成轻量级锁如果轻量级锁膨胀失败了就继续往上锁膨胀

    流程图如下仅只有偏向锁逻辑

    源码论证

    首先我们先需要知道Synchronized底层源码的入口在哪里在字节码层面表示为monitorentermonitorexit字节码指令而我们知道JVM是负责执行字节码最终转换成不同CPU平台的ISA指令集也称之为跨平台。而JVM执行字节码分为

    1. CPP解释执行

    1. 模板解释执行汇编

    1. JIT编译执行

    一级一级的优化而最根本是CPP解释执行后者都是基于CPP解释执行的不断优化后者的难度极大所以读者弄明白CPP解释执行就即可。

    在Hotspot源码中CPP解释执行的入口在bytecodeInterpreter.cpp文件这里要注意JDK1.8不同版本对synchronized关键字实现有区别所以本文选的是jdk8u40版本其他版本可能没有偏向锁等等逻辑

    首先读者明白使用Synchronized关键字时需要一个锁对象而底层就是操作这个锁对象的对象头所以我们先从markOop.hpp文件中找到对象头的描述信息是不是跟外面8股文描述的一模一样呢😏

    对象头熟悉以后源码中就是操作对象头不同的锁状态设置不同对象头用对象头来表示不同的锁状态替换对象头的原子性依靠CAS来保证。如果存在并发那么CAS竞争失败的线程就会往下走一步一步的锁升级反而如果没有竞争那就默认使用偏向锁。

    下面是Hotspot中C++解释器对于monitorenter字节码指令的解释执行源码注释特别详细。

    CASE(_monitorenter): {
            // 拿到锁对象
            oop lockee = STACK_OBJECT(-1);
            // derefing's lockee ought to provoke implicit null check
            CHECK_NULL(lockee);
            // find a free monitor or one already allocated for this object
            // if we find a matching object then we need a new monitor
            // since this is recursive enter
            // 从当前线程栈中找到一个没有被使用的BasicObjectLock
            // 作用用来记录锁状态
            BasicObjectLock* limit = istate->monitor_base();
            BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
            BasicObjectLock* entry = NULL;
            while (most_recent != limit ) {
              if (most_recent->obj() == NULL) entry = most_recent;
              else if (most_recent->obj() == lockee) break;
              most_recent++;
            }
            
            if (entry != NULL) {
              // 抢坑为什么这里不需要CAS因为属于线程栈线程变量线程安全。
              entry->set_obj(lockee);
              int success = false;
    
              // 得到epoch的掩码。
              uintptr_t epoch_mask_in_place = (uintptr_t)markOopDesc::epoch_mask_in_place;
    
              // 得到当前锁对象的对象头。
              markOop mark = lockee->mark();
    
              intptr_t hash = (intptr_t) markOopDesc::no_hash;
              
              // implies UseBiasedLocking
              // 当前是偏向锁模式可以用JVM参数UseBiasedLocking控制
              if (mark->has_bias_pattern()) {
                uintptr_t thread_ident;
                uintptr_t anticipated_bias_locking_value;
                thread_ident = (uintptr_t)istate->thread();
    
                // lockee->klass()->prototype_header() 是否拿到对象的类模板的头部信息。
                // lockee->klass()->prototype_header() | thread_ident) 是类模板头部信息组合上线程id
                // mark 是当前锁对象的头部信息。
                // markOopDesc::age_mask_in_place 是当前对象的年龄信息。
                // 所以与年龄无关
                // 所以拿锁对象的原型对象的对象头控制
                // lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark 如果为0 代表当前对象头偏向锁偏向了当前线程
                anticipated_bias_locking_value =
                  (((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) &
                  ~((uintptr_t) markOopDesc::age_mask_in_place);
    
                // 等于0代表当前锁对象头部和类模板头部一样。
                // 所以这是一次偏向锁的命中。
                if  (anticipated_bias_locking_value == 0) {
                  // already biased towards this thread, nothing to do
                  if (PrintBiasedLockingStatistics) {
                    (* BiasedLocking::biased_lock_entry_count_addr())++;
                  }
                  success = true;
                }
                // 当前对象头已经膨胀成轻量级或者重量级锁了。也即非偏向锁。
                else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) {
                  // try revoke bias
                  // 尝试撤销偏向锁
                  markOop header = lockee->klass()->prototype_header();
                  if (hash != markOopDesc::no_hash) {
                    header = header->copy_set_hash(hash);
                  }
                  // CAS尝试取消偏向
                  if (Atomic::cmpxchg_ptr(header, lockee->mark_addr(), mark) == mark) {
                    if (PrintBiasedLockingStatistics)
                      (*BiasedLocking::revoked_lock_entry_count_addr())++;
                  }
                }
    
                // 来到这里可能表示当前偏向于其他线程。
                // 而epoch发生了变动表示批量撤销偏向锁了。
                // 当前线程可以尝试争抢一次偏向锁没有成功就去锁升级
                else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {
                  // try rebias
                  // 尝试重偏向
                  markOop new_header = (markOop) ( (intptr_t) lockee->klass()->prototype_header() | thread_ident);
                  if (hash != markOopDesc::no_hash) {
                    new_header = new_header->copy_set_hash(hash);
                  }
                  // CAS竞争重偏向。
                  if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), mark) == mark) {
                    if (PrintBiasedLockingStatistics)
                      (* BiasedLocking::rebiased_lock_entry_count_addr())++;
                  }
                  // CAS失败锁升级
                  else {
                      // 锁升级逻辑
                    CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
                  }
                  success = true;
                }
                // 来到这里表示当前是匿名偏向锁也即暂时还没有线程占用
                // 或者是已经偏向了某个线程所以这里CAS尝试一次
                else {
                  // try to bias towards thread in case object is anonymously biased
                  markOop header = (markOop) ((uintptr_t) mark & ((uintptr_t)markOopDesc::biased_lock_mask_in_place |
                                                                  (uintptr_t)markOopDesc::age_mask_in_place |
                                                                  epoch_mask_in_place));
                  if (hash != markOopDesc::no_hash) {
                    header = header->copy_set_hash(hash);
                  }
                  markOop new_header = (markOop) ((uintptr_t) header | thread_ident);
                  // debugging hint
                  DEBUG_ONLY(entry->lock()->set_displaced_header((markOop) (uintptr_t) 0xdeaddead);)
    
                  // 如果是匿名偏向这个CAS就有可能成功
                  // 如果是已经偏向其他线程这个CAS不能成功直接往锁升级走
                  if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header) {
                    if (PrintBiasedLockingStatistics)
                      (* BiasedLocking::anonymously_biased_lock_entry_count_addr())++;
                  }
                  // cas失败
                  else {
                      // 锁升级逻辑
                    CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
                  }
                  success = true;
                }
              }
    
              // traditional lightweight locking
              // case1:如果当前已经锁升级了
              // case2:如果当前不支持偏向锁
              if (!success) {
                markOop displaced = lockee->mark()->set_unlocked();
                entry->lock()->set_displaced_header(displaced);
                bool call_vm = UseHeavyMonitors;
                // UseHeavyMonitors是JVM参数是否直接开启重量级锁
                // 如果不直接开启就CAS竞争轻量级锁竞争成功就直接返回
                if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
                  // Is it simple recursive case?
                  // CAS失败可能是锁重入如果不是锁重入那么就是竞争失败要往锁升级逻辑走了。
                  if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
    
                      // 轻量级锁的锁重入
                    entry->lock()->set_displaced_header(NULL);
                  } else {
                      // 锁升级逻辑
                    CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
                  }
                }
              }
              UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
            } else {
              istate->set_msg(more_monitors);
              UPDATE_PC_AND_RETURN(0); // Re-execute
            }
          }

    要明白偏向锁对应的对象头的几个部分的意义然后带入到源码中就比较容易理解。

    • 线程对象偏向于那个线程当没有线程对象时就代表是匿名偏向此时线程都可以去竞争

    • epoch是否发生了批量锁撤销为什么要锁撤销因为偏向锁升级为轻量级锁就需要撤销

    • 偏向锁标志位0表示无锁1表示偏向锁偏向锁和无锁的锁标志位都是01

    • 锁标志位表示不同锁状态偏向锁表示为01要注意无锁也是表示为01所以需要额外的偏向锁标志位来区分是无锁还是偏向锁

    总结

    可能源码部分一直是一个难点操作的内容太多了并且还是C++实现的。但是从对象头的角度去分析理解还是很有帮助。从一篇文章弄懂源码级别的内容是比较困难的所以笔者在某站李哈zzz有视频讲解。

    最后如果本帖对您有一定的帮助希望能点赞+关注+收藏您的支持是给我最大的动力后续会一直更新各种框架的使用和框架的源码解读~

  • 阿里云国际版折扣https://www.yundadi.com

  • 阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6