【学习底层原理系列】Java底层-synchronized锁-2偏向锁篇

摘要:
本文将分为以下两个部分:第一部分:介绍MarkWord和LockRecord数据结构。这些知识是理解同步关键字的基本原理的关键。在对象标头(如哈希码)中也是如此。它不是当前线程ID,已锁定或正在竞争。还将在对象所属的类中维护epoch值。这里我们简称它为cEpoch。比较两者:epoch小于类的cEpoch值,并应用锁。如果epoch<cEpoch,则表示已发生批重偏置,并且当前锁定对象已“释放”。请注意,不会修改MW中的threadId。如果我们要设计这个同步锁,我们肯定会制定一些策略来弥补成本。

上一篇通过构建金字塔结构,来从不同的角度,由浅入深的对synchronized关键字做了介绍,

快速跳转:https://www.cnblogs.com/xyang/p/11631866.html

本文将从底层实现的各个“组件”着手,详细拆解其工作原理。

本文会分为以下2节内容:

  第一节:介绍MarkWord和LockRecord两种数据结构,该知识点是理解synchronized关键字底层原理的关键。

  第二节:分析偏向锁加锁解锁时机和过程

一.先来了解两种数据结构,你应该了解这些知识点

1.MarkWord:在锁的使用过程中会对锁对象作出相应的操作

 在HotSpot虚拟机中,Java对象在内存中存储的布局,分为三个部分:对象头,实例数据,对齐填充。

本文重点关注对象头。

对象头又划分为2或3部分,具体包括:

  1. MarkWord(后文简称MW,后续详细介绍)
  2. 类型指针:指向这个对象所属的类的元数据(klass)的指针
  3. 最后这一部分比较特殊,只有在对象是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,用于指向锁对象。

 上述两者的关系用下图表示:

【学习底层原理系列】Java底层-synchronized锁-2偏向锁篇第1张

二.偏向锁怎么工作

在对象创建的时候,MW会有一个初始态,要么是无锁态,要么是初始偏向锁态(ThreadId、epoch值都为初始值0)。程序员的世界不存在二义性,最终总会选一个,选择的依据是虚拟机的配置参数,在JDK1.6以后,默认是开启的,如果要禁用掉:-XX:-UseBiasedLocking。

什么时候需要禁用呢?如果能确认程序在大多数情况下,都存在多线程竞争,那么就可以禁用掉偏向锁。没必要每次都走一遍偏向锁->轻量级锁->重量级锁的完整升级流程。

1.先放一张图,直观的描述偏向锁的加锁、解锁、撤销基本流程

【学习底层原理系列】Java底层-synchronized锁-2偏向锁篇第2张

2.加锁过程

 步骤一:

  1. LR记录赋值:在当前线程的栈中,申请一个LR,把obj指向锁对象

步骤二:如图中所示,线程T1,执行到同步代码,尝试加偏向锁,【判断三要素:标志位,ThreadId,epoch】:

  1. 先用标志位判断是否支持偏向锁。锁对象的对象头MW区域后3个bit位的值是101。特别需要注意:如果是001,是无锁状态,代表偏向锁不可用,会走加轻量级锁流程。
  2. 再用ThreadId值,决定加锁还是重入还是竞争
    1. 0值加锁。如果ThreadId=0,代表无任何线程持有该对象的偏向锁,可以执行加锁操作,进入加锁流程;
    2. 非0值,
      1. 且为当前线程id,重入。如果ThreadId!=0,就判断其值是否是当前线程的ID,分两种情况:如果是,直接锁重入,不再重复加锁。
      2. 不为当前线程id,加锁或竞争。非0值,说明是其他线程(图中T2)已获得了同步锁。对象所属Class里也会维护一个epoch值,这里我们简称为cEpoch,比较两者:
        1. epoch小于类的cEpoch值,加锁。如果epoch<cEpoch,说明发生过批量重偏向,当前锁对象已被“释放”了。此时进行“重偏向”(里说的释放并非真正意义的释放,而是隐含着一层意思:当前线程已经执行完同步块,且在某次重偏向操作中,也检测到这一点,不再维护epoch的最新值,这样新的线程认为此时该偏向锁,可以加锁,直接CAS修改ThreadId即可)
        2. epoch等于类的cEpoch值,竞争锁。

    标准的可加锁状态MW内容如下图所示:

    

锁类型

25bit

 

4bit

1bit

2bit

 

23bit

2bit

 

是否偏向锁

锁标志位

偏向锁

ThreadId==0

epoch==n

分代年龄

1

01

    或者

锁类型

25bit

 

4bit

1bit

2bit

 

23bit

2bit

 

是否偏向锁

锁标志位

偏向锁

ThreadId!=0

epoch==n(小于cepoch)

分代年龄

1

01

第二步:通过CAS原子操作,把T1的ThreadId写入MW。执行结果有两种情况:

  1. 写入成功,获得偏向锁,进入同步代码块执行同步逻辑。
  2. 写入失败,表明在第一步判断和CAS操作之间,有其他线程已获得了锁。走锁竞争逻辑。

2.解锁过程

当前线程执行完同步代码块后,进行解锁,解锁操作比较简单,仅仅将栈中的最近一条LR中的obj赋值为null。这里需要注意,MW中的threadId并不会做修改。

【学习底层原理系列】Java底层-synchronized锁-2偏向锁篇第3张

3.锁竞争处理流程

  T1尝试加锁,

  如果存在锁竞争情况,持有锁的线程T2并不会在发现竞争的第一时间就直接撤销锁,或者升级锁,而是执行到安全点后再处理。

    1.   此时如果当前线程已执行完同步块代码线程已不存活,将会撤销锁至无锁状态,然后进入锁升级逻辑。
    2.   否则,将会走锁升级流程,升级为轻量级锁,且升级完后T2继续持有轻量级锁,继续执行同步代码。

【学习底层原理系列】Java底层-synchronized锁-2偏向锁篇第4张

  ps:怎么判断是否还在执行同步代码呢?遍历栈中的RL,如果都为null,代表锁已全部释放。

4.批量重偏向和批量撤销

有这样一种场景:如果我们预判竞争不多,大部分情况下是单一线程执行同步块,开启了偏向锁。但是在实际使用环境中,出现了大量的竞争,这时候怎么办呢?停机重新配置参数?恐怕不是最好的方案。如果是我们来设计这个这个Synchronized锁,肯定也会做一些兜底策略。比如这样来做,当某一事件发生了N次,那么就更改一下处理策略?

是的,基本思想差不多,只不过更完善,暂时留一个悬念,在下次揭晓。

免责声明:文章转载自《【学习底层原理系列】Java底层-synchronized锁-2偏向锁篇》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇鸟叔私房菜的第三天vscode如何调试node项目(给node项目打断点)下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

docker原理(转)

转自:https://zhuanlan.zhihu.com/p/22382728      https://zhuanlan.zhihu.com/p/22403015 在学习docker的过程中,我发现目前docker学习最大的障碍,不是网上的资源太少,而是网上的资源太多,资源太多带来的噪声让学习效率降低不少。而在讲解docker原理上,所有的讲解都是关于...

Java线程间通信-回调的实现方式

Java线程间通信-回调的实现方式   Java线程间通信是非常复杂的问题的。线程间通信问题本质上是如何将与线程相关的变量或者对象传递给别的线程,从而实现交互。   比如举一个简单例子,有一个多线程的类,用来计算文件的MD5码,当多个这样的线程执行的时候,将每个文件的计算的结果反馈给主线程,并从控制台输出。   线程之间的通讯主要靠回调来实现,回调的概念说...

设置Kali Linux虚拟机连接网络

设置KaliLinux虚拟机连接网络当用户在一个系统中进行工作时,连接网络是必不可少的工作。大学霸IT达人在VMware虚拟机中,可用的网络连接模式有桥接模式、NAT模式、仅主机模式和自定义网络连接模式。如果仅实现虚拟机访问互联网的话,建议使用NAT模式。如果希望虚拟机与物理机也可以进行通信的话,则需要使用桥接模式。具体方法如下所示:(1)在菜单栏中,依次...

libvirt

1.什么是libvirt 虚拟云实现的三部曲:虚拟化技术实现-->虚拟机管理-->集群资源管理(云管理)。各种不同的虚拟化技术都提供了基本的管理工具。比如,启动,停用,配置,连接控制台等。这样在构建云管理的时候就存在两个问题: 1) 如果采用混合虚拟技术,上层就需要对不同的虚拟化技术调用不同管理工具,很是麻烦。 2) 虚拟化技术发展很迅速,系...

VMware vCenter6.7配置并验证虚拟机的高可用

一、实验 1、拓扑图  2、实验设计图 二、虚拟机高可用性实验 1、新建ISCSI存储 2、输入存储卷名称 3、指定存储卷大小 4、新建ISCSI目标 5、点击下一步 6、添加目标IP地址,可以添加多个 7、点击下一步 8、点击创建   9、vCenter上添加软件适配器 10、动态发现设备 11、添加发送目标服务器 12、重...

令牌桶、漏斗、冷启动限流在sentinel的应用

 分布式系统为了保证系统稳定性,在服务治理的限流中会根据不同场景进行限流操作,常见的限流算法有: 令牌桶:可容忍一定突发流量的速率的限流,令牌桶算法的原理是系统以恒定的速率产生令牌,然后把令牌放到令牌桶中,令牌桶有一个容量,当令牌桶满了的时候,再向其中放令牌,那么多余的令牌会被丢弃;当想要处理一个请求的时候,需要从令牌桶中取出一个令牌,如果此时令牌桶中...