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

作者简介:
专注于研究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)有视频讲解。

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
### 回答1: synchronized关键字的底层实现原理涉及到Java对象头的概念。在Java对象头中,有一个表示锁状态的标志位,它用来标识对象的锁状态。当线程进入一个synchronized方法或代码块时,会尝试获取对象的锁。如果该锁没有被其他线程占用,则该线程会成功获取锁并进入临界区。如果该锁已经被其他线程占用,则该线程会进入阻塞状态,直到锁被释放。当线程执行完synchronized方法或代码块后,会释放锁。 在JVM中,synchronized关键字实现的锁有两种,分别为偏向锁和重量级锁。偏向锁是一种优化机制,它在对象创建时会将锁标志位初始化为偏向模式。当一个线程获取该对象的锁时,会将当前线程的ID记录在对象头中,并将锁标志位设置为偏向模式。以后该线程再次获取该对象的锁时,无需竞争,可以直接获取。重量级锁则是一种比较传统的锁机制,它使用操作系统的互斥量来实现锁。当多个线程竞争同一对象的锁时,会进入阻塞队列,等待锁被释放。 因此,synchronized关键字的底层实现原理就是通过Java对象头中的标志位来实现锁状态的记录和判断,并通过偏向锁和重量级锁来优化锁的竞争。 ### 回答2: synchronizedJava中用来实现线程同步的关键字,它保证了在同一时间只有一个线程可以进入被synchronized修饰的代码块或方法。synchronized的底层实现原理涉及到Java对象头、Monitor、线程间通信等。 每个Java对象在内存中都会有一个对象头,对象头中包含了一些元数据字段,其中有一个字段用来记录当前对象的锁信息。当一个线程进入synchronized代码块时,首先会尝试对对象加锁,如果对象的锁信息表明已经被其他线程锁定,则该线程会进入阻塞状态,等待其他线程释放锁。如果对象的锁信息表明还没有被其他线程锁定,则将对象头中的锁信息设置为该线程,并且将一个Monitor关联到该对象上。 Monitor是Java中用来实现监视器锁的机制,它与每个Java对象关联。Monitor内部维护了一个线程等待队列和一个拥有锁的线程。每个Monitor对象只能拥有一个线程,其他线程需要获取锁时只能进入等待队列。当某个线程执行完synchronized代码块或方法时,会释放锁,并且唤醒等待队列中的一个线程来竞争锁。 线程间的通信是通过底层的wait()、notify()和notifyAll()方法实现的。当一个线程执行wait()方法时,它会释放锁并进入阻塞状态,等待其他线程调用notify()或notifyAll()方法来唤醒它。唤醒的线程将进入就绪状态,并与其他线程竞争锁,竞争成功后将继续执行。 总结起来,synchronized的底层实现原理是通过Java对象头、Monitor和线程间的通信来实现的。它保证了在同一时间只有一个线程可以进入被synchronized修饰的代码块或方法,避免了多个线程对共享资源的并发访问造成的数据不一致问题。 ### 回答3: synchronizedJava中用于实现线程同步的关键字,可以用于修饰方法或代码块,保证多个线程对同一资源进行访问时的互斥。 synchronized的底层实现原理是基于对象的监视器(Monitor)机制。在Java中的每一个对象都会有一个与之关联的Monitor对象,Monitor对象用于同步对共享资源的访问。当一个线程遇到synchronized修饰的代码块或方法时,它首先需要获得对象的Monitor对象的锁。若锁已经被其他线程持有,则该线程会进入阻塞状态,直到锁被释放。当该线程获得锁之后,它就可以执行临界区内的代码了。 当一个线程执行完synchronized代码块或方法后,会释放对Monitor对象的锁,其他处于等待的线程就有机会获得锁,进入临界区执行代码。这样就保证了在任意时刻,只有一个线程可以获得锁,其他线程需要等待,实现了对共享资源的互斥访问。 synchronized通过内置的锁机制来实现线程间的同步,确保了数据的一致性和完整性。它基于底层的Monitor机制利用了操作系统的原子性操作,保证了多线程并发执行时的正确性。但是,在synchronized的机制下,一个线程获得了对象的锁之后,其他线程必须等待,可能会造成线程的阻塞和延迟。此外,在一些特殊情况下,可能会出现死锁的问题,即多个线程相互等待对方释放锁。 总之,synchronized是一种可靠的线程同步机制,通过Monitor对象的锁机制实现对共享资源的互斥访问。它的底层实现原理是基于对象的监视器(Monitor)机制,利用锁和等待队列来控制线程的执行和互斥访问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员李哈

创作不易,希望能给与支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值