【Java多线程】2——synchronized底层原理

2 synchronized底层原理

⭐⭐⭐⭐⭐⭐
Github主页👉https://github.com/A-BigTree
笔记仓库👉https://github.com/A-BigTree/tree-learning-notes
个人主页👉https://www.abigtree.top
⭐⭐⭐⭐⭐⭐


如果可以,麻烦各位看官顺手点个star~😊

如果文章对你有所帮助,可以点赞👍收藏⭐支持一下博主~😆


文章目录

  • 2 synchronized底层原理
    • 2.1 锁膨胀机制
      • 2.1.1 JDK 1.6分水岭
      • 2.1.2 锁膨胀
        • 无锁
        • 偏向锁
        • 轻量级锁
        • 重量级锁
      • 2.1.3 锁消除
      • 2.1.4 自旋锁
      • 2.1.5 自适应自旋锁
    • 2.2 对象存储结构
      • 2.2.1 对象存储结构
        • 对象头
        • 对象体
        • 对齐字节
      • 2.2.2 Mark Word
        • 无锁状态
        • 偏向锁状态
        • 轻量级锁状态
        • 重量级锁状态
        • GC状态
      • 2.2.3 对象监视器
        • 锁对象和对象监视器之间的关系
        • 监视器对象结构
    • 2.3 加锁和解锁流程
      • 2.3.1 `monitorenter`指令和`monitorexit`指令
        • 字节码指令分析
        • 对应关系
        • 执行过程
      • 2.3.2 可重入性
      • 2.3.3 为什么monitorexit出现两次
      • 2.3.4 小结

为了方便大家理解,我们先说结论:synchronized底层的锁机制并不是『用钥匙开锁』这样的模型,而是『小狗撒尿』。

  • 小狗:线程 ;
  • 电线杆子:锁对象关联的监视器对象 ;
  • 小狗撒的尿:成功拿到锁的线程在监视器对象身上留下的痕迹;

2.1 锁膨胀机制

2.1.1 JDK 1.6分水岭

在 JDK 1.6 之前,synchronized 的底层工作机制只有『重量级锁』这一种模式。从 JDK 1.6 开始,官方对synchronized 的底层工作机制做了重大调整。

之所以要做这个调整是考虑到线程访问锁时,如果没有竞争,那么加锁和解锁的操作就显得有点浪费性能。

为了减少获得锁和释放锁带来的性能消耗,引入了『偏向锁』和『轻量级锁』的概念。升级后锁一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级,也称为膨胀过程不可逆

2.1.2 锁膨胀

无锁

对象(用来作为锁对象的那个)刚创建出来,没有线程来申请锁——没有线程准备执行同步代码块。

偏向锁

只有一个线程访问对象。此时没有必要执行获得锁和释放锁的操作,我们只需要在对象中记录当前偏向的线程的 ID,只要是这个线程来访问对象,则无需获得锁,直接可以开始操作。

如果一个线程获得了锁,那么锁就进入偏向模式,此时 Mark Word 的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作(同步操作就是加锁、解锁操作的总称),即获取锁的过程只需要检查 Mark Word 的锁标记位为偏向锁以及当前线程 ID 等于 Mark Word 的 Thread ID 即可,这样就省去了大量有关锁申请的操作。

轻量级锁

轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块

重量级锁

重量级锁是由轻量级锁升级而来。当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。此时各个线程之间存在竞争关系。

重量级锁一般用在不追求吞吐量,同步块或者同步方法执行时间较长的场景。

2.1.3 锁消除

占用锁的线程把同步代码执行完释放了锁,并且没有其它线程来申请锁,锁对象就会从重量级锁状态回到无锁状态。

2.1.4 自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。大致相当于:在遇到 synchronized 时,没有拿到锁,但是并不乖乖去阻塞,而是继续执行一些无意义代码。执行完这些代码再看看别人把锁释放没有。如果还是没有释放,那就只好去阻塞了。

  • 适合的场景:锁定时间较短,通过自旋有较大几率获得锁。
  • 不适合的场景:锁定时间长,自旋操作本身浪费了 CPU 性能。
    • 通俗来说就是:“旋”了半天没等到,白“旋”了。

2.1.5 自适应自旋锁

这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。

2.2 对象存储结构

学习本节内容是为了搞清楚当线程在申请锁时,底层具体是如何工作的。

2.2.1 对象存储结构

现在我们都知道了对象(包括数组)都是放在堆内存中,那么对象在堆内存中保存时的数据结构是什么样子的呢?

在这里插入图片描述

对象头
  • 非数组对象
    • Mark Word
    • 类型指针
  • 数组
    • Mark Word
    • 类型指针
    • 数组长度

在这里插入图片描述

对象体

这一部分也称为:实例数据。也就是对象中实际包含的属性数据。

对齐字节

为了寻址方便,JVM 要求每一个对象起始地址必须是 8 字节的整数倍,也就是要求每一个对象的存储空间都是 8 字节的整数倍。所以对于占空间不满足这一要求的对象会进行填充。所以这一部分是不一定存在的。

2.2.2 Mark Word

Mark Word 部分用于存储对象自身运行时数据,如哈希码、GC分代年龄等,这部分数据的长度在 32 位和 64 位的虚拟机中分别为 32 位和 64 位。

由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到 JVM 的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间。

在 Mark Word 各种状态下的数据结构中,我们首先要关注一个数据:锁标识位。

锁标识位数值是否偏向锁标识位含义
010无锁状态
011偏向锁状态
00——轻量级锁状态
10——重量级锁状态
11——GC状态
无锁状态

在这里插入图片描述

偏向锁状态

在这里插入图片描述

轻量级锁状态

在这里插入图片描述

重量级锁状态

在这里插入图片描述

GC状态

在这里插入图片描述

锁状态膨胀到 『重量级锁』 的时候,就会有一个指向 『对象监视器』 的指针。

2.2.3 对象监视器

从上面的介绍中我们发现:当锁状态膨胀为『重量级锁』时,Mark Word 中有一个指针指向一个特殊的对象——对象监视器。这个对象是由C++开发的,原名ObjectMonitor。

锁对象和对象监视器之间的关系

在这里插入图片描述

其中 this 就是我们这里所说的锁对象。而就是通过这个锁对象的对象头,我们一步一步找到了它关联的 ObjectMonitor 对象监视器对象。

监视器对象结构
ObjectMonitor() {_header       = NULL;_count        = 0;  // 锁计数器_waiters      = 0;_recursions   = 0; // 锁的重入次数_object       = NULL;_owner        = NULL; // 指向持有 ObjectMonitor 对象的线程_WaitSet      = NULL; // 处于 wait 状态的线程,会被加入到 _WaitSet(等待队列)_WaitSetLock  = 0 ;_Responsible  = NULL ;_succ         = NULL ;_cxq          = NULL ;FreeNext      = NULL ;_EntryList    = NULL ; // 处于等待锁 block 状态的线程,会被加入到该列表(阻塞队列)_SpinFreq     = 0 ;_SpinClock    = 0 ;OwnerIsThread = 0 ;}

2.3 加锁和解锁流程

2.3.1 monitorenter指令和monitorexit指令

字节码指令分析

代码如下:

public class Demo06CodeTest {public void doSth() {synchronized (this) {System.out.println("Hello");}}}

编译并javap分析

> javac Demo06CodeTest.java
> javap -c Demo06CodeTest.class

分析结果如下:

Compiled from "Demo06CodeTest.java"  
public class com.atguigu.thread.demo.Demo06CodeTest {  
public com.atguigu.thread.demo.Demo06CodeTest();  
Code:  
0: aload_0  
1: invokespecial #1 // Method java/lang/Object."<init>": ()V
4: return  public void doSth();  
Code:  
0: aload_0  
1: dup  
2: astore_1  
3: monitorenter  
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;  
7: ldc #3 // String Hello  
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V  
12: aload_1  
13: monitorexit  
14: goto 22  
17: astore_2  
18: aload_1  
19: monitorexit  
20: aload_2  
21: athrow  
22: return  
Exception table:  
from to target type  
4 14 17 any  
17 20 17 any  
}
对应关系

在这里插入图片描述

执行过程

根据锁对象找到对象监视器:

在这里插入图片描述

加锁或阻塞:

在这里插入图片描述

2.3.2 可重入性

从一个同步代码块又进入下一个同步代码块,这种现象我们称之为:『重入』。而多线程执行过程中如果不支持∶『可重入』,那将会发生死锁的问题。

所谓可重入性就是指:一个线程在获得锁之后如果再次进入同步代码,那么对于由它自己锁定的对象可以直接获得锁。具体来说就是:

  • _owner 保持指向当前线程不变
  • _count 继续 + 1
  • _recursions 继续 + 1

而当内层的 synchronized 需要释放锁时执行:

  • _owner 保持指向当前线程不变
  • _count - 1
  • _recursions - 1

当最外层的 synchronized 需要释放锁时执行:

  • _owner 擦除
  • _count - 1,让 _count 回到 0
  • _recursions - 1,让 _recursions 回到 0

结论:可重入性就是指一个线程可以直接获得它自己加的锁。

2.3.3 为什么monitorexit出现两次

在这里插入图片描述

2.3.4 小结

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

淘宝APP详情数据抓取技术揭秘:用Python实现自动化数据获取(附代码实例)

获取淘宝APP详情数据接口通常涉及到网络爬虫技术&#xff0c;因为淘宝作为一个大型电商平台&#xff0c;其数据并不直接对外公开提供API接口供第三方开发者使用。然而&#xff0c;通过模拟浏览器行为或使用淘宝开放平台提供的API&#xff08;如果有的话&#xff09;&#xff0c…

SpringBoot3的RabbitMQ消息服务

目录 预备工作和配置 1.发送消息 实现类 控制层 效果 2.收消息 3.异步读取 效果 4.Work queues --工作队列模式 创建队列text2 实体类 效果 5.Subscribe--发布订阅模式 效果 6.Routing--路由模式 效果 7.Topics--通配符模式 效果 异步处理、应用解耦、流量削…

Java八股文(SpringCloud Alibaba)

Java八股文のSpringCloud Alibaba SpringCloud Alibaba SpringCloud Alibaba Spring Cloud Alibaba与Spring Cloud有什么区别&#xff1f; Spring Cloud Alibaba是Spring Cloud的衍生版本&#xff0c;它是由Alibaba开发和维护的&#xff0c;相比于Spring Cloud&#xff0c;它在…

JavaScript Uncaught ReferenceError: WScript is not defined

项目场景&#xff1a; 最近在Visual Studio 2019上编译libmodbus库&#xff0c;出现了很多问题&#xff0c;一一解决特此记录下来。 问题描述 首先就是configure.js文件的问题&#xff0c;它会生成两个很重要的头文件modbus_version.h和config.h&#xff0c;这两个头文件其中…

如何使用Docker轻松构建和管理应用程序(二)

上一篇文章介绍了 Docker 基本概念&#xff0c;其中镜像、容器和 Dockerfile 。我们使用 Dockerfile 定义镜像&#xff0c;依赖镜像来运行容器&#xff0c;因此 Dockerfile 是镜像和容器的关键&#xff0c;Dockerfile 可以非常容易的定义镜像内容&#xff0c;同时在我们后期的微…

day53 动态规划part10

121. 买卖股票的最佳时机 简单 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票&#xff0c;并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。 返回你可…

【Redis】Redis 介绍Redis 为什么这么快?Redis数据结构Redis 和Memcache区别 ?为何Redis单线程效率也高?

目录 Redis 介绍 Redis 为什么这么快&#xff1f; Redis数据结构 Redis 和Memcache区别 &#xff1f; 为何Redis单线程效率也高&#xff1f; Redis 介绍 Redis 是一个开源&#xff08;BSD 许可&#xff09;、基于内存、支持多种数据结构的存储系统&#xff0c;可以作为数据…

如何本地部署Elasticsearch+cpolar实现公网查询与管理内网数据

文章目录 系统环境1. Windows 安装Elasticsearch2. 本地访问Elasticsearch3. Windows 安装 Cpolar4. 创建Elasticsearch公网访问地址5. 远程访问Elasticsearch6. 设置固定二级子域名 正文开始前给大家推荐个网站&#xff0c;前些天发现了一个巨牛的 人工智能学习网站&#xff…

音视频处理 - 音频概念详解,码率,采样率,位深度,声道,编码

1. 音频采样 与视频不同&#xff0c;音频的最小单位不是一帧&#xff0c;而是一个采样。 采样是当前一刻声音的声音样本&#xff0c;样本需要经过数字转换才能存储为样本数据。 真实声音是连续的&#xff0c;但是在计算机中&#xff0c;声音是离散且均匀的声音样本。 2. 位深…

电阻的妙用:限流、分压、滤波,助力电路设计!

电阻可以降低电压&#xff0c;这是通过电阻的分压来实现的。事实上&#xff0c;利用电阻来降低电压只是电阻的多种功能之一。电路中的电阻与其他元件&#xff08;电容、电感&#xff09;结合用于限流、滤波等。&#xff08;本文素材来源&#xff1a;https://www.icdhs.com/news…

正则表达式 vs. 字符串处理:解析优势与劣势

title: 正则表达式 vs. 字符串处理&#xff1a;解析优势与劣势 date: 2024/3/27 15:58:40 updated: 2024/3/27 15:58:40 tags: 正则起源正则原理模式匹配优劣分析文本处理性能比较编程应用 1. 正则表达式起源与演变 正则表达式&#xff08;Regular Expression&#xff09;最早…

Windows的Linux子系统迁移

默认 Windows 的 Linux 子系统&#xff08;WSL&#xff09;安装的 C 盘&#xff0c;如果有用作 Docker 镜像制作很容易磁盘不够用。可采取如下步骤迁移&#xff08;以下在 Windows PowerShell 中进行的&#xff0c;但在 CMD.exe 中执行效果相同&#xff09;&#xff1a; 1、执…