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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/163076.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Linux】 iptables 入门简介

文章目录前言作用持久化和恢复执行的顺序前言 简单地说,iptables是Linux的防火墙程序。它将使用表监控进出服务器的流量。这些表包含称为链的规则集,这些规则将过滤传入和传出数据包。 作用 当数据包与规则匹配的时候,会为其指定一个目标&a…

基于幂等表思想的幂等实践

一、为什么需要幂等 分布式场景下,多个业务系统间实现强一致的协议是极其困难的。一个最简单和可实现的假设就是保证最终一致性,这要求服务端在处理一个重复的请求时需要给出相同的回应,同时不会对持久化数据产生副作用(即多次操…

【Linux】Linux下调试器gdb的使用

👑作者主页:安 度 因 🏠学习社区:StackFrame 📖专栏链接:Linux 文章目录一、前言二、铺垫三、指令集和使用1、指令集2、演示四、结语如果无聊的话,就来逛逛 我的博客栈 吧! 🌹 一、前…

通信原理与MATLAB(十三):AMI的编解码

目录1.AMI的的编解码原理1.1 AMI编码原理1.2 AMI解码原理2.AMI编解码的代码3.AMI编解码结果图4.AMI的误码率曲线4.1 原理4.2 AMI的误码率曲线代码4.3 误码率曲线图1.AMI的的编解码原理 1.1 AMI编码原理 如下图所示,AMI的编码原理:将原始码元的1转换成1,0转换成-1。…

快过年了,用Python康康哪一家足浴店可以带朋友去玩.....

人生苦短,我用Python 首先肯定是去正经足浴店, 毕竟一年出差也不少, 大家都很辛苦, 好不容易放假了, 约上好兄弟一起去放松放松~ 所需环境 python 3.8 解释器pycharm 编辑器 所需模块 requests 数据来源分析 …

Silane-PEG-NH2 氨基聚乙二醇硅烷 NH2-PEG-Silane结构式

英文名称:Silane-PEG-NH2 Silane-PEG-Amine 中文名称:硅烷-聚乙二醇-氨基 分子量:1k,2k,3.4k,5k,10k,20k。。。 存储条件:-20C,避光,避湿 用 途…

2022年度总结,迎接2023

目录 我和CSDN的2022 初次见面: 你我的成长: 博客: 比赛: 我和CSDN的2023 我和CSDN的2022 初次见面: CSDN你好啊!我跟你的初次见面在于2022年4月2日!!! 这这半年内…

【算法5.1】背包问题 - 01背包 (至多最大价值、至少最小价值)

目录 至少模板和至多模板的两大区别 1、至多模板 2、至少模板 2. 01背包 - 至多模板 - 体积至多j,总价值最大 1、朴素做法 - 二维dp 2、优化 - 一维dp 4700. 何以包邮? - 至少模板 - 价值至少j,总价值最小 至少模板和至多模板的两大区…

list容器与vector容器的区别

vector与list都是STL中非常重要的序列式容器,它们都存放在namespace std命名空间中,由于俩个容器的底层结构不同,导致其特性不同 一、底层实现结构不同 vector本质是一段动态连续的顺序表,而list底层是一个双向循环链表 二、访…

Ubuntu多硬盘luks全盘加密自动解锁(硬件变更后失效)的方法

简介大家都知道,Linux现在用Luks全盘加密一直有一个痛点,就是每次开机都需要输入解密硬盘的密码,之后又要输入用户密码,非常的麻烦!本文正是为了解决这个问题诞生的!本文多硬盘加密带来的效果是&#xff0c…

Redis持久化——RDB机制详解

在运行情况下,Redis 以数据结构的形式将数据维持在内存中,为了让这些数据在 Redis 重启之后仍然可用,需要将数据写入持久存储 持久化是指将数据写入持久存储,例如固态磁盘(SSD) Redis 提供了一系列持久化选项。这些包括&#xff1…

Java-黑马Java学习作业-day07综合练习

学习视频链接:黑马Java学习视频 文章目录练习一:飞机票练习二:打印素数练习三:验证码练习四:复制数组练习五:评委打分练习六:数字加密练习七:数字解密练习八:抽奖解法一:…

【C++11】—— 可变参数模板

目录 一、可变参数模板概念以及定义方式 二、参数包的展开 1. 递归函数方式展开参数包 2. 逗号表达式展开参数包 三、STL容器中的empalce相关接口函数 一、可变参数模板概念以及定义方式 在c11之前,类模板和函数模板只能含有固定数量的模板参数,c11…

JavaScript高级 ES5 面向对象原型继承

原型以及ES5中实现继承1. 对象和函数的原型1. 普通对象的原型 [[prototype]]2. 函数的原型 prototype2. new、constructor1. new 操作符2. constructor属性3. 将方法放到原型上4. 创建对象的内存表现5. 重写原型对象3. 原型链的查找顺序4. 原型链实现的继承5. 借用构造函数继承…

深入URP之Shader篇10: 深度值专题(1)

之前研究Unlit shader的时候就遇到一些Z值相关的问题,一笔带过了,比如ComputeFogFactor中的UNITY_Z_0_FAR_FROM_CLIPSPACE。今天就把URP Shader中出现的Z相关的问题做一个专题一起研究下。 深度缓冲的方向和UNITY_REVERSED_Z 先说这个关于z的宏&#x…

nacos:服务注册与发现

导入SpringCloudAlibaba相关的依赖&#xff0c;并在父工程将依赖进行管理 <dependencyManagement> <dependencies> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-s…

Java EE|多线程代码实例之定时器与线程池

文章目录&#x1f534;定时器什么是定时器以及开发中的作用标准库中的定时器定时器的实现&#x1f534;线程池什么是线程池标准库中的线程池创建一个线程池ThreadPoolExecutor构造方法解析线程池的实现&#x1f534;定时器 什么是定时器以及开发中的作用 程序中的定时器功能与…

【互联网大厂机试真题 - 华为】九宫格

题目描述 九宫格是一款广为流传的游戏,起源于河图洛书。游戏规则是:1到9九个数字放在3x3的格子中,要求每行、每列以及两个对角线上的三数之和都等于15. 在金麻名著《射雕英雄传》中黃蓉曾给九宫格的一种解法,口诀:戴九恩一,左三右七,二四有肩,八六为足,五居中央。解法…

【云原生进阶之容器】第四章Operator原理4.3节--Operator模式

1 Operator概述 1.1 诞生背景 Kubernetes实际是期望状态管理器。先在Kubernetes中指定应用程序期望状态(实例数,磁盘空间,镜像等),然后它会尝试把应用维持在这种状态。Kubernetes的控制平面运行在Master节点上,它包含数个controller以调和应用达到期望状态: 检查当前的…

【阶段三】Python机器学习30篇:机器学习项目实战:智能推荐系统的基本原理与计算相似度的常用方法

本篇的思维导图: 智能推荐系统模型 智能推荐系统属于非监督式学习,是机器学习一个非常重要的应用领域,它能带来的经济价值往往是直接且非常可观的。 智能推荐系统的基本原理 智能推荐系统的应用场景 互联网每天都在产生海量信息,用户行为数据呈现爆发式增长…