当前位置: 首页 > news >正文

【嗅探底层】你知道Synchronized作用是同步加锁,可你知道它在JVM中是如何实现的吗?...


​本文系公众号石杉的架构笔记的读者投稿

作者:李瑞杰

目前任职于阿里巴巴,资深JVM研究人员


友情提示:

本文内容涉及JVM底层,文章烧脑,请谨慎阅读!


我们可以利用synchronized关键字来对程序进行加锁。它既可以用来声明一个synchronized代码块,也可以直接标记静态方法或者实例方法。

当谈到synchronized时,我们有必要了解字节码中的monitorenter和monitorexit指令。

这两种指令均会消耗操作数栈上的一个引用类型的元素(也就是 synchronized关键字括号里的引用),作为所要加锁解锁的锁对象。

下面我们将深入了解Synchronized在JVM底层的实现原理。

考察以下的代码:

查看这个代码编译后的字节码,我就直接用下面这张图解释了。

ps:截图截得不太好,下面有点没截到,大家凑合看看:


你可能会留意到,上面的字节码中包含一个 monitorenter 指令以及多个 monitorexit 指令。

这是因为 Java 虚拟机需要确保所获得的锁在正常执行路径以及异常执行路径上都能够被解锁。

大家可以看我的注释,自己思考一下,应该都能看懂。

应该注意,如果用synchronized标记方法,你会看到字节码中方法的访问标记包括ACC_SYNCHRONIZED。

该标记表示在进入该方法时,Java 虚拟机需要进行monitorenter操作。

而在退出该方法时,不管是正常返回,还是向调用者抛异常,Java 虚拟机均需要进行monitorexit操作。

用两张图一看就懂。



可以看到,在0号字节码处就返回了。

这里有人可能问了,这里没有调用monitorenter和monitorexit指令啊?怎么实现的加锁?

要注意,这里monitorenter 和 monitorexit 操作所对应的锁对象是隐式的。

对于实例方法来说,这两个操作对应的锁对象是 this;对于静态方法来说,这两个操作对应的锁对象则是所在类的Class实例。


我们先来介绍Synchronized的重入的实现机理。


可以认为每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

当执行monitorenter时,如果目标锁对象的计数器为0,那么说明它没有被其他线程所持有。

Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

在目标锁对象的计数器不为 0 的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为0,代表锁已被释放。


这就是锁的重入的实现机理。


说完了这个实现机理,我们来探究具体的锁实现。

首先谈谈重量级锁,重量级锁是 Java 虚拟机中最为基础的锁实现。

在这种状态下,Java 虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。在Linux中,这是通过pthread库的互斥锁来实现的。

此外,这些操作将涉及系统调用,需要从操作系统的用户态切换至内核态,其开销非常之大。

为了尽量避免昂贵的线程阻塞、唤醒操作,Java虚拟机会在线程进入阻塞状态之前,以及被唤醒后竞争不到锁的情况下,进入自旋状态,在处理器上空跑并且轮询锁是否被释放。

如果此时锁恰好被释放了,那么当前线程便无须进入阻塞状态,而是直接获得这把锁。

下面我将介绍自适应自旋的概念,刚才说了自旋是什么,但是自旋很耗费资源,所以我们可以根据以往自旋等待时是否能够获得锁,来动态调整自旋的时间(循环数目)。

所以Synchronized是否公平这个问题可以休矣,为什么呢?

处于阻塞状态的线程,并没有办法立刻竞争被释放的锁。然而,处于自旋状态的线程,则很有可能优先获得这把锁。所以Synchronized不是公平的

我们再介绍轻量级锁,针对多个线程在不同的时间段请求同一把锁,也就是说没有锁竞争。

针对这种情形,Java 虚拟机采用了轻量级锁,来避免重量级锁的阻塞以及唤醒。

在介绍轻量级锁的原理之前,我们先来了解一下Java虚拟机是怎么区分轻量级锁和重量级锁的。

简单的说,对象头中有一个标记字段。它的最后两位便被用来表示该对象的锁状态,其中:

  • 00代表轻量级锁

  • 01代表无锁(或偏向锁)

  • 10代表重量级锁

  • 11则跟垃圾回收算法的标记有关。


当进行加锁操作时,Java虚拟机会判断是否已经是重量级锁。

如果不是,它会在当前线程的当前栈桢中划出一块空间,作为该锁的锁记录,并且将锁对象的标记字段复制到该锁记录中。

然后,Java 虚拟机会尝试用 CAS 操作替换锁对象的标记字段。

各位有兴趣可以了解一下JVM的CAS在X86机器上的实现,是汇编指令lock cmpxhcg

这里我简单介绍一下,CAS 是一个原子操作,它会比较目标地址的值是否和期望值相等,如果相等,则替换为一个新的值。

假设当前锁对象的标记字段为 X…XYZ,Java 虚拟机会比较该字段是否为 X…X01。

如果是,则替换为刚才分配的锁记录的地址。由于内存对齐的缘故,它的最后两位为 00。此时,该线程已成功获得这把锁,可以继续执行了。

如果不是 X…X01,那么有两种可能:

  • 第一,该线程重复获取同一把锁。此时,Java 虚拟机会将0加入锁记录,以代表该锁被重复获取。

  • 第二,其他线程持有该锁。此时,Java 虚拟机会将这把锁膨胀为重量级锁,并且阻塞当前线程。

你可以将一个线程的所有锁记录想象成一个栈结构,每次加锁压入一条锁记录,解锁弹出一条锁记录,当前锁记录指的便是栈顶的锁记录。

当进行解锁操作时,如果当前锁记录的值为 0,则代表重复进入同一把锁,直接返回即可。

若当前锁记录不是0,Java 虚拟机会尝试用 CAS 操作,比较锁对象的标记字段的值是否为当前锁记录的地址。

如果是,则替换为锁记录中的值,也就是锁对象原本的标记字段。此时,该线程已经成功释放这把锁。

如果不是,则意味着这把锁已经被膨胀为重量级锁。此时,Java 虚拟机会进入重量级锁的释放过程,唤醒因竞争该锁而被阻塞了的线程。

下面我们介绍偏向锁,偏向锁针对的是从始至终只有一个线程请求某一把锁。是轻量级锁的更进一步的乐观情况。

在线程进行加锁时,如果该锁对象支持偏向锁,那么 Java 虚拟机会通过 CAS 操作,将当前线程的地址记录在锁对象的标记字段之中,并且将标记字段的最后三位设置为 101。

这里介绍一下epoch的概念,每个类中维护一个epoch值,你可以理解为这个类所有实例对象的第几代偏向锁。

当设置偏向锁时,Java 虚拟机需要将该epoch值复制到锁对象的标记字段中。我们规定,你加的偏向锁的代数高,是可以把代数低的PK下去的。


接下来我给你讲的过程,你就知道为什么要这么设计了。


我们先从偏向锁的撤销讲起。

当请求加锁的线程和锁对象标记字段保持的线程地址不匹配时(而且epoch即代数必须相等,如若不等,那么当前线程可以将该锁重偏向至自己,因为新的epoch的代数肯定要高于以前的代数),Java 虚拟机需要撤销该偏向锁。

这个撤销过程非常麻烦,它要求持有偏向锁的线程到达安全点,再将偏向锁替换成轻量级锁。

在宣布某个类的偏向锁失效时,Java 虚拟机实则将该类的epoch值加 1,表示之前那一代的偏向锁已经失效。而新设置的偏向锁则需要使用类中的最新epoch代数来加锁。

为了保证当前持有偏向锁并且已加锁的线程不至于因此丢锁,Java虚拟机需要遍历所有线程的Java栈,找出该类已加锁的实例,并且将它们标记字段中的 epoch值加1。该操作需要所有线程处于安全点状态。

所以有专家近年来提出,偏向锁在锁竞争激烈的情况下,非但不能优化性能,反而可能伤害应用性能。

如果总撤销数超过另一个阈值(对应 Java 虚拟机参数-XX:BiasedLockingBulkRevokeThreshold,默认值为 40),那么 Java 虚拟机会认为这个类已经不再适合偏向锁。

此时,Java 虚拟机会撤销该类实例的偏向锁,并且在之后的加锁过程中直接为该类实例设置轻量级锁。


END


欢迎长按下图关注公众号:石杉的架构笔记!

公众号后台回复资料,获取作者独家秘制学习资料

石杉的架构笔记,BAT架构经验倾囊相授


相关文章:

  • Android App性能优化技能,看这篇就够了
  • 花费上万转行当程序员,却找不到工作,程序员吐槽:这技术白学了
  • [PHP]实体类基类和序列化__sleep问题
  • Loj #2570. 「ZJOI2017」线段树
  • 我理解的CLH
  • Java并发——结合CountDownLatch源码、Semaphore源码及ReentrantLock源码来看AQS原理
  • 走进 JDK 之 LinkedList
  • Python Day19
  • random,json,pickle,hashlib,shutil,hmac,shelve 模块
  • 支付宝架构师眼中的高并发架构 阅读笔记
  • 【一起学爬虫】scrapy框架的安装
  • java 实现DFA 算法(理论百度搜索)
  • v-lazyload数据变化图片不切换
  • 记录微博爬虫遇到问题
  • 一张思维导图辅助你深入了解 Vue | Vue-Router | Vuex 源码架构
  • 【编码】-360实习笔试编程题(二)-2016.03.29
  • 〔开发系列〕一次关于小程序开发的深度总结
  • Cookie 在前端中的实践
  • Java 多线程编程之:notify 和 wait 用法
  • Java|序列化异常StreamCorruptedException的解决方法
  • java小心机(3)| 浅析finalize()
  • JWT究竟是什么呢?
  • Python学习之路16-使用API
  • RxJS: 简单入门
  • vue从创建到完整的饿了么(11)组件的使用(svg图标及watch的简单使用)
  • Web Storage相关
  • 闭包--闭包作用之保存(一)
  • 老板让我十分钟上手nx-admin
  • 浅谈JavaScript的面向对象和它的封装、继承、多态
  • 手写一个CommonJS打包工具(一)
  • 智能合约开发环境搭建及Hello World合约
  • 最近的计划
  • Mac 上flink的安装与启动
  • #Java第九次作业--输入输出流和文件操作
  • #中的引用型是什么意识_Java中四种引用有什么区别以及应用场景
  • (007)XHTML文档之标题——h1~h6
  • (173)FPGA约束:单周期时序分析或默认时序分析
  • (4) openssl rsa/pkey(查看私钥、从私钥中提取公钥、查看公钥)
  • (带教程)商业版SEO关键词按天计费系统:关键词排名优化、代理服务、手机自适应及搭建教程
  • (附源码)springboot美食分享系统 毕业设计 612231
  • (附源码)ssm智慧社区管理系统 毕业设计 101635
  • (免费领源码)Java#ssm#MySQL 创意商城03663-计算机毕业设计项目选题推荐
  • (学习日记)2024.04.04:UCOSIII第三十二节:计数信号量实验
  • (转)http协议
  • (转)利用PHP的debug_backtrace函数,实现PHP文件权限管理、动态加载 【反射】...
  • .gitignore文件---让git自动忽略指定文件
  • .Net 6.0 处理跨域的方式
  • .NET/C# 反射的的性能数据,以及高性能开发建议(反射获取 Attribute 和反射调用方法)
  • .net分布式压力测试工具(Beetle.DT)
  • .Net组件程序设计之线程、并发管理(一)
  • @NestedConfigurationProperty 注解用法
  • [16/N]论得趣
  • [2019.3.5]BZOJ1934 [Shoi2007]Vote 善意的投票
  • [AMQP Connection 127.0.0.1:5672] An unexpected connection driver error occured
  • [android] 练习PopupWindow实现对话框