【从刷面试题到构建知识体系】Java底层-synchronized锁-2偏向锁篇
上一篇通过构建金字塔结构,来从不同的角度,由浅入深的对synchronized关键字做了介绍,
快速跳转:https://www.cnblogs.com/xyang/p/11631866.html
本文将从底层实现的各个“组件”着手,详细拆解其工作原理。
本文会分为以下4节内容:
第一节:介绍MarkWord和LockRecord两种数据结构,该知识点是理解synchronized关键字底层原理的关键。
第二节:分析偏向锁加锁解锁时机和过程
一.先来了解两种数据结构,你应该了解这些知识点
1.MarkWord:在锁的使用过程中会对锁对象作出相应的操作
在HotSpot虚拟机中,Java对象在内存中存储的布局,分为三个部分:对象头,实例数据,对齐填充。
本文重点关注对象头。
对象头又划分为2或3部分,具体包括:
- MarkWord(后文简称MW,后续详细介绍)
- 类型指针:指向这个对象所属的类的元数据(klass)的指针
- 最后这一部分比较特殊,只有在对象是Java数组时才会存在,记录的是数组的长度。为什么要存在这个记录呢?我们知道,在普通Java对象中,我们可以通过读取对象所属类的元数据,计算出对象的大小。而数组是无法做到的,于是借助这块区域来记录。
本文重点关注MW区域
MW是一块固定大小内存区域,在32位虚拟机中是32个bit,对应的,64位虚拟机中是64个bit。本文以32位虚拟机为例分析。
我们从直观上理解,所谓的头信息,一般都是用来记载一些不易变的信息,例如在http请求头中的各种头信息。在对象头中也是如此,例如hashcode。在JVM虚拟机中为了解决存储空间开销,对象头的MW大小已经固定。那么,要存储的信息有比较多,包括且不限于:锁标志位、GC信息、锁相关信息,总大小远远超出32bit,怎么办呢?
共享存储区域,在不同的时刻,根据需求存储需要的信息。
请参考下图:
|
锁类型 |
25bit |
4bit |
1bit |
2bit |
|
|---|---|---|---|---|---|
|
23bit |
2bit |
是否偏向锁 |
锁标志位 |
||
|
无锁 |
对象hashcode |
分代年龄 |
0 |
01 |
|
|
偏向锁 |
线程ID |
epoch |
分代年龄 |
1 |
01 |
|
轻量级锁 |
指向栈中锁记录的指针 |
00 |
|||
|
重量级锁 |
指向互斥量 |
10 |
|||
|
GC标记 |
空 |
11 |
|||
说明:两个标志位最多只能标识4个状态,那么剩下一个怎么办?共享。无锁和偏向锁共享01状态,他们两个的区分
2.LockRecord:
在当前线程的栈中申请LR(LockRecord简称,下同),主要包含两部分,第一步部分可以用于存放MW的副本;第二部分obj,用于指向锁对象。
上述两者的关系用下图表示:

二.偏向锁怎么工作
在对象创建的时候,MW会有一个初始态,要么是无锁态,要么是初始偏向锁态(ThreadId、epoch值都为初始值0)。程序员的世界不存在二义性,最终总会选一个,选择的依据是虚拟机的配置参数,在JDK1.6以后,默认是开启的,如果要禁用掉:-XX:-UseBiasedLocking。
什么时候需要禁用呢?如果能确认程序在大多数情况下,都存在多线程竞争,那么就可以禁用掉偏向锁。没必要每次都走一遍偏向锁->轻量级锁->重量级锁的完整升级流程。
1.基本流程

2.加锁过程
步骤一:
- LR记录赋值:在当前线程的栈中,申请一个LR,把obj指向锁对象
步骤二:如图中所示,线程T1,执行到同步代码,尝试加偏向锁,首先会做【偏向锁是否可用】的判断:
- 锁对象的对象头MW区域后3个bit位的值是101。特别需要注意:如果是001,是无锁状态,代表偏向锁不可用,会走加轻量级锁流程。
- ThreadId值:
- 如果ThreadId=0,代表无任何线程持有该对象的偏向锁,可以执行加锁操作,进入加锁流程;
- 如果ThreadId!=0,就判断其值是否是当前线程的ID,分两种情况:如果是,直接锁重入,不再重复加锁。如果否,说明是其他线程(图中T2)已获得了同步锁,进入“第三步”锁竞争流程。
- epoch值:对象所属Class里也会维护一个epoch值,这里我们简称为cEpoch,对该值的判断,可能会导致两种操作:
- 如果epoch<cEpoch,且ThreadId!=0,说明发生过批量重偏向,当前锁对象已被“释放”了。此时进行“重偏向”(里说的释放并非真正意义的释放,而是隐含着一层意思:当前线程已经执行完同步块,且在某次重偏向操作中,也检测到这一点,不再维护epoch的最新值,这样新的线程认为此时该偏向锁,可以加锁,直接CAS修改ThreadId即可)。
- 如果ThreadId==0,因为偏向锁没有显示的撤销修改ThreadId过程,说明肯定是初始状态,那么epoch值也肯定是初始状态0,此时直接进行加锁操作。
可加锁状态的MW内容如下图所示:
|
锁类型 |
25bit |
4bit |
1bit |
2bit |
|
|
23bit |
2bit |
是否偏向锁 |
锁标志位 |
||
|
偏向锁 |
ThreadId==0 |
epoch==n |
分代年龄 |
1 |
01 |
以上三个点都判断通过,进入“第二步”,加锁流程
第二步:通过CAS原子操作,把T1的ThreadId写入MW。执行结果有两种情况:
- 写入成功,获得偏向锁,进入同步代码块执行同步逻辑。
- 写入失败,表明在第一步判断和CAS操作之间,有其他线程已获得了锁。走锁竞争逻辑。
2.解锁过程
当前线程执行完同步代码块后,进行解锁,解锁操作比较简单,仅仅将栈中的最近一条LR中的obj赋值为null。这里需要注意,MW中的threadId并不会做修改。

3.锁竞争处理流程
持有锁的线程T2并不会在发现竞争的第一时间就直接撤销锁,或者升级锁,而是执行到安全点后再处理。
- 此时如果当前线程已执行完同步块代码且线程已不存活,将会撤销锁,将锁对象恢复至无锁状态,然后进入锁升级逻辑。
- 如果当前线程同步块还未执行完或者线程依然存活,将会走锁升级流程,升级为轻量级锁,且升级完后T2继续持有轻量级锁,继续执行同步代码。

ps:怎么判断是否还在执行同步代码呢?遍历栈中的RL,如果都为null,代表锁已全部释放。
4.批量重偏向和批量撤销
有这样一种场景:如果我们预判竞争不多,大部分情况下是单一线程执行同步块,开启了偏向锁。但是在实际使用环境中,出现了大量的竞争,这时候怎么办呢?停机重新配置参数?恐怕不是最好的方案。如果是我们来设计这个这个Synchronized锁,肯定也会做一些兜底策略。比如这样来做,当某一事件发生了N次,那么就更改一下处理策略?
是的,基本思想差不多,只不过更完善,暂时留一个悬念,在下次揭晓。
【从刷面试题到构建知识体系】Java底层-synchronized锁-2偏向锁篇的更多相关文章
- 【从刷面试题到构建知识体系】Java底层-synchronized锁-1
在技术论坛中,经常看到一种言论:面试造火箭,干活拧螺丝.我们平时写的大部分代码的确是CRDU,再提一个层次,也无非就是揉进去复杂一些的业务逻辑,把一堆的CRDU组合起来. 那么问题来了:我们提倡的研究 ...
- 通过面试题学习零散知识:Java面试题整理
一.如何看待面试题 对于喜欢学习的开发者来说,我们抛开工作和生活的时间,剩余的时间并不多,如果都用于学习的话,也不可能学的下所有感兴趣的技术点,精力也跟不上,我是深感如是.而面试题一般都是零碎的知识 ...
- Java知识体系
Java知识体系 java知识结构.jpg web框架.jpg 计算机课程体系.png 2016-08-19_090929.png 流行的哈希算法生存状况.jpg "JAVA之父" ...
- 最强最全的Java后端知识体系
目录 最全的Java后端知识体系 Java基础 算法和数据结构 Spring相关 数据库相关 方法论 工具清单 文档 @(最强最全的Java后端知识体系) 最全的Java后端知识体系 最全的Java后 ...
- github上最全的资源教程-前端涉及的所有知识体系
前面分享了前端入门资源汇总,今天分享下前端所有的知识体系. 个人站长对个人综合素质要求还是比较高的,要想打造多拉斯自媒体网站,不花点心血是很难成功的,学习前端是必不可少的一个环节, 当然你不一定要成为 ...
- github上最全的资源教程-前端涉及的所有知识体系【转】
github上最全的资源教程-前端涉及的所有知识体系[转自:蓝猫的博客] 综合类 综合类 地址 前端知识体系 http://www.cnblogs.com/sb19871023/p/3894452.h ...
- Atitit 知识图谱解决方案:提供完整知识体系架构的搜索与知识结果overview
Atitit 知识图谱解决方案:提供完整知识体系架构的搜索与知识结果overview 知识图谱的表示和在搜索中的展1 提升Google搜索效果3 1.找到最想要的信息.3 2.提供最全面的摘要.4 ...
- android知识体系
1.Android架构分为4层*应用程序层 Android会同一系列核心应用程序包一起发布,该应用程序包包括email客户端,SMS短消息程序,日历,地图,浏览器,联系人管理程序等.所有的应用程序都是 ...
- unity3d所要知道的基础知识体系大纲,可以对照着学习,不定期更新
本文献给,想踏入3D游戏客户端开发的初学者. 毕业2年,去年开始9月开始转作手机游戏开发,从那时开始到现在一共面的游戏公司12家,其中知名的包括搜狐畅游.掌趣科技.蓝港在线.玩蟹科技.天神互动.乐元素 ...
随机推荐
- 由"跨域"引出的一个终极思想(jsonp)
1.什么是跨域? 当协议.子域名.主域名.端口号中任意一个不相同时,都算作不同域. 跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,但是因为浏览器存在一个"同源策略&q ...
- 基于WeChat的消息存储备份、远程控制、小功能项目开源分享计划
WeChat+ 关于该项目 起源 该项目的起因是一个比较程(老)序(油)猿(条)的理由,有一天我发现我下班时间比较早,有点尴尬,但是又不想没事干还坐在公司,那么如何解决我的问题呢,初步想法是远程控制电 ...
- 详解http报文
摘要 作为一个web开发者,每天都在使用者Http协议,却总是一知半解.本文参看Http RFC7230规范,梳理了http报文部分. http 报文构成 start-line: 起始行,描述请求或响 ...
- Ubuntu 启动zookeeper报错
在启动zk客户端连接server时报错: 2019-03-30 23:06:24,915 [myid:localhost:2181] - INFO [main-SendThread(localhost ...
- eclipse快捷键——纪念我突然失忆的脑子
常用快捷键 1.快速new出来一个对象 Alt+/ 2.快速格式化代码 Ctrl+I 3.快速导入包 Ctrl+Shift+O
- SpringBootSecurity学习(13)前后端分离版之JWT
JWT 使用 前面简单介绍了把默认的页面登录改为前后端分离的接口异步登录的方法,可以帮我们实现基本的前后端分离登录功能.但是这种基本的登录和前面的页面登录还有一个一样的地方,就是使用session和c ...
- Angular 自定义管道
管道的作用就是将原始值进行转化处理,转换为所需要的值: 1. 新建sex-reform.pipe.ts文件 ng g pipe sex-reform 2. 编辑sex-reform.pipe.ts文件 ...
- SpringBoot进阶教程(六十二)整合Kafka
在上一篇文章<Linux安装Kafka>中,已经介绍了如何在Linux安装Kafka,以及Kafka的启动/关闭和创建发话题并产生消息和消费消息.这篇文章就介绍介绍SpringBoot整合 ...
- 十大排序算法JavaScript实现总结
花费了几周的时间断断续续的练习和模仿与使用JavaScript代码实现了十大排序算法. 里面有每种算法的动图和静态图片演示,看到图片可以自己先按照图片的思路实现一下. github中正文链接,点击查看 ...
- 基本IO操作--字节流
一.InputStream与OutputStream1. 输入与输出 我们编写的程序除了自身会定义一些数据信息外,经常还会引用外界的数据,或是将自身的数据发送到外界.比如,我们编写的程序想读取一个文本 ...