【Java并发知识总结 | 第五篇】深入理解Synchronized底层原理(Monitor对象、Synchronized锁优化)

在这里插入图片描述

文章目录

  • 5.深入理解Synchronized底层原理(Monitor对象、Synchronized锁优化)
    • 5.1Synchronized的特性
      • 5.1.1原子性
      • 5.1.2可见性
      • 5.1.3有序性
      • 5.1.4可重入性
    • 5.2Synchronized的用法
    • 5.3Synchronized的两种同步方式
      • 4.3.1同步代码块
      • 5.3.2同步方法
    • 5.4Synchronized的底层实现
      • 5.4.1 monitorenter、monitorexit指令和ACC_SYNCHRONIZED标志
        • (1)monitorenter指令
        • (2)monitorexit指令
        • (3)ACC_SYNCHRONIZED标志
        • (4)总结
      • 5.4.2 monitor监视器
        • (1)操作系统的管程
        • (2)ObjectMonitor
        • (3)Java Monitor的工作机理
      • 5.4.3 Java对象头与monitor关联
        • (1)Java对象头
        • (2)Mark Word
        • (2)对象头和monitor关联
    • 5.5Synchronized的优化
      • 5.5.1锁膨胀
      • 5.5.2偏向锁
      • 5.5.3轻量级锁
      • 5.5.4重量级锁
      • 5.5.5锁消除
      • 5.5.6锁粗化
      • 5.5.7自旋锁和自适应自旋锁
    • 5.6常见面试题
      • 5.6.1Synchronized和Lock两者区别?
      • 5.6.2Synchronized和ReentrantLock两者区别?

参考文章链接:
Synchronized解析——如果你愿意一层一层剥开我的心
深入理解synchronized底层原理,一篇文章就够了!

5.深入理解Synchronized底层原理(Monitor对象、Synchronized锁优化)

5.1Synchronized的特性

  • Synchronized有以下四个特性:原子性、可见性、有序性、可重入性

5.1.1原子性

  • 原子性:指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
  • 被synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放,这中间的过程无法被中断(除了已经废弃的stop()方法),即保证了原子性。

5.1.2可见性

  • 可见性:指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的
  • 一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到主存当中,保证资源变量的可见性,如果某个线程占用了该锁,其他线程就必须在锁池中等待锁的释放。

5.1.3有序性

  • Synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。

  • Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。

5.1.4可重入性

  • 当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,
  • 但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。

5.2Synchronized的用法

  1. Synchronized可以修饰的地方

    • 修饰静态方法
    • 修饰成员函数
    • 直接定义代码块

    image-20240326150207416

  2. 但是归根结底它上锁的资源只有两类:

    • 一个是 对象
    • 一个是
  3. 普通的成员函数,归该类的实例对象所有,即若需要访问该方法,需要拿到该类的实例对象相应的锁;

  4. 被static修饰的静态方法、静态属性归类所有,即若需要访问该方法,需要拿到该类相应的锁;

public class Testl {private int i=0;private static int j=0;private final Testl instance = new Test1();//对成员函数加锁,必须获得该类的实例对象的锁才能进入同步块public synchronized void add1(){i++;}//对静态方法加锁,必须获得类的锁才能进入同步块public static synchronized void add2(){i++;   }public void method(){// 同步块,执行前必须获得Test1类的锁synchronized(Testl.class){}//同步块,执行前必须先获得实例对象的锁synchronized(instance){}}
}

5.3Synchronized的两种同步方式

  1. Synchronized有两种形式上锁,一个是对方法上锁,一个是构造同步代码块。
  2. 他们的底层实现其实都一样,在进入同步代码之前先获取锁,获取到锁之后锁的计数器+1同步代码执行完,锁的计数器-1,如果获取失败就阻塞式等待锁的释放。
  3. 只是他们在同步块识别方式上有所不一样,从class字节码文件可以表现出来
    • 同步方法通过方法flags标志,
    • 同步代码块monitorenter和monitorexit指令操作。

4.3.1同步代码块

image-20240326150940681

反编译,可得如下图:

image-20240326150956120

  1. 由图可得,添加了synchronized关键字的代码块,多了两个指令**monitorenter、monitorexit**。即JVM使用monitorenter和monitorexit两个指令实现同步。
  2. 同步块是由monitorenter指令进入,然后monitorexit释放锁
  3. 在执行monitorenter之前需要尝试获取锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器加1当执行monitorexit指令时,锁的计数器也会减1。当获取锁失败时会被阻塞,一直等待锁被释放。
  4. 第二个monitorexit是来处理异常的,仔细看反编译的字节码,正常情况下第一个monitorexit之后会执行goto指令,而该指令转向的就是23行的return,也就是说正常情况下只会执行第一个monitorexit释放锁,然后返回。而如果在执行中发生了异常,第二个monitorexit就起作用了,它是由编译器自动生成的,在发生异常时处理异常然后释放掉锁。

5.3.2同步方法

image-20240326150416061

反编译,可得如下图:

image-20240326150433433

由图可得,添加了synchronized关键字的方法,多了ACC_SYNCHRONIZED标记。即JVM通过在方法访问标识符(flags)中加入ACC_SYNCHRONIZED来实现同步功能

5.4Synchronized的底层实现

5.4.1 monitorenter、monitorexit指令和ACC_SYNCHRONIZED标志

(1)monitorenter指令
  1. 每个对象都与一个monitor 相关联。当且仅当拥有所有者时(被拥有),monitor才会被锁定。
  2. 执行到monitorenter指令的线程,会尝试去获得对应的monitor,如下:
    • 每个对象维护着一个记录着被锁次数的计数器, 对象未被锁定时,该计数器为0。线程进入monitor(执行monitorenter指令)时,会把计数器设置为1
    • 当同一个线程再次获得该对象的锁的时候,计数器再次自增.
  3. 当其他线程想获得该monitor的时候,就会阻塞,直到计数器为0才能成功。

image-20240326152514500

(2)monitorexit指令
  1. monitor的拥有者线程才能执行 monitorexit指令。
  2. 线程执行monitorexit指令,就会让monitor的计数器减一
  3. 如果计数器为0,表明该线程不再拥有monitor。其他线程就允许尝试去获得该monitor了。

image-20240326152618288

(3)ACC_SYNCHRONIZED标志
  1. 方法级别的同步是隐式的,作为方法调用的一部分。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。
  2. 调用一个设置了ACC_SYNCHRONIZED标志的方法,执行线程==需要先获得monitor锁==,然后开始执行方法,方法执行之后再释放monitor锁,当方法不管是正常return还是抛出异常都会释放对应的monitor锁。
  3. 在这期间,如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。
  4. 如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

image-20240326152800585

(4)总结
  1. 同步代码块是通过monitorenter和monitorexit来实现,当线程执行到monitorenter的时候要先获得monitor锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。
  2. 同步方法是通过中设置ACC_SYNCHRONIZED标志来实现,当线程执行有ACC_SYNCHRONI标志的方法,需要获得monitor锁
  3. 每个对象维护一个加锁计数器,为0表示可以被其他线程获得锁,不为0时,只有当前锁的线程才能再次获得锁。
  4. 同步方法和同步代码块底层都是通过monitor来实现同步的。
  5. 每个对象都与一个monitor相关联,线程可以占有或者释放monitor。

5.4.2 monitor监视器

  1. 一种同步工具,或者说是同步机制,它通常被描述成一个对象
  2. 操作系统的管程是概念原理,ObjectMonitor是它的原理实现。

img

(1)操作系统的管程
  1. 管程 (英语:Monitors,也称为监视器) 是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源
  2. 这些共享资源一般是硬件设备或一群变量。管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。
  3. 与那些通过修改数据结构实现互斥访问的并发程序设计相比,管程实现很大程度上简化了程序设计。
  4. 管程提供了一种机制,线程可以临时放弃互斥访问,等待某些条件得到满足后,重新获得执行权恢复它的互斥访问。
(2)ObjectMonitor

在Java虚拟机(HotSpot)中,Monitor(管程)是由ObjectMonitor实现的,其主要数据结构如下:

image-20240326154435898

ObjectMonitor中几个关键字段的含义如图所示:

img

(3)Java Monitor的工作机理

img

  1. 想要获取monitor的线程,首先会进入_EntryList队列。
  2. 某个线程获取到对象的monitor后,进入Owner区域,设置为当前线程,同时计数器_count加1。_
  3. 如果线程调用了wait()方法,则会进入WaitSet队列。它会释放monitor锁,即将owner赋值为null,count自减1,进入WaitSet队列阻塞等待。
  4. 如果其他线程调用 notify() / notifyAll() ,会唤醒WaitSet中的某个线程,该线程再次尝试获取monitor锁,成功即进入_Owner区域
  5. 同步方法执行完毕了,线程退出临界区,会将monitor的owner设为null,并释放监视锁。

5.4.3 Java对象头与monitor关联

(1)Java对象头

在JVM中,对象是分成三部分存在的:对象头、实例数据、对其填充。

img

  1. 实例数据:对象真正存储的有效信息,存放类的属性数据信息,包括父类的属性信息;
  2. 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
  3. 对象头:Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)
  4. 对象头,是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。
    • 对象头主要结构是由Mark WordClass Metadata Address组成
    • 其中Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息
    • Class Metadata Address类型指针指向对象的类元数据 ,JVM通过该指针确定该对象是哪个类的实例
(2)Mark Word
  1. Mark Word 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。
  2. 在32位的HotSpot虚拟机中,如果对象处于未被锁定的状态下
    • 那么Mark Word的32bit空间里的25位用于存储对象哈希;
    • 4bit用于存储对象分代年龄;
    • 2bit用于存储锁标志位
    • 1bit固定为0,表示非偏向锁。其他状态如下图所示:
  3. 锁标志位(2bit):
    • 01:该对象为无锁状态
    • 00:该对象为轻量级锁,指向栈中锁记录的指针
    • 10:重量级锁,指向互斥量的指针

img

(2)对象头和monitor关联

img

  • 对象怎么和monitor实现关联
  1. 对象里有对象头
  2. 对象头里面有Mark Word
  3. Mark Word指针指向了monitor

5.5Synchronized的优化

  1. JDK6的时候,新增了两个锁状态(偏向锁、轻量级锁),通过锁消除、锁粗化、自旋锁等方法使用各种场景,给synchronized性能带来了很大的提升。

5.5.1锁膨胀

上面讲到锁有四种状态,并且会因实际情况进行膨胀升级,其膨胀方向是:无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。

5.5.2偏向锁

  1. 作用:减少统一线程获取锁的代价。在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时就是偏向锁
  2. 核心思想让同一个线程一直拥有同一个锁,直到出现竞争,才去释放锁
  3. 举例:如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查 Mark Word 的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。

5.5.3轻量级锁

  1. 轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。
  2. 注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。
  3. 使用轻量级锁,那些等待竞争锁的线程不需要切 换到阻塞状态,只需等一等(自旋),等持有锁 的线程释放锁,即可获取锁

5.5.4重量级锁

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

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

image-20240326161403407

5.5.5锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。 比如下面代码的method1和method2的执行效率是一样的,因为object锁是私有变量,不存在所得竞争关系。

img

5.5.6锁粗化

锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。比如下面method3经过锁粗化优化之后就和method4执行效率一样了。

img

5.5.7自旋锁和自适应自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。

自旋锁:许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。

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

5.6常见面试题

5.6.1Synchronized和Lock两者区别?

SynchronizedLock
形态不同java关键字、jvm层次接口
锁的释放不同1.执行完同步代码,自动释放锁
2.发生异常,jvm释放锁
1.手动释放锁 unlock()
2.在finally里必须释放,不然会死锁
锁类型不同可重入、非公平可重入、可公平(非公平)
是否可以尝试获取锁不可以可以,tryLock()
粒度

5.6.2Synchronized和ReentrantLock两者区别?

SynchronizedReentrantLock
锁类型不同非公平锁非公平锁、公平锁
锁的释放不同1.执行完同步代码,自动释放锁
2.发生异常,jvm释放锁
手动释放锁
是否可以尝试获取锁不可以可以,tryLock()
是否可以超时获取锁不支持可以,tryLock(time)
是否可响应中断不支持,不可响应线程的interrupt信号支持,lockInterruptibly()
性能较差比Synchronized优20%

在这里插入图片描述

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

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

相关文章

脏牛提权(靶机复现)

目录 一、脏牛漏洞概述 二、漏洞复现 1.nmap信息收集 1.1.查看当前IP地址 1.2.扫描当前网段,找出目标机器 1.3.快速扫描目标机全端口 三、访问收集到的资产 192.168.40.134:80 192.168.40.134:1898 四、msf攻击 1.查找对应exp 2.选择对应exp并配置相关设…

uniApp中使用小程序XR-Frame创建3D场景(2)加载模型

上篇文章讲述了如何将XR-Frame作为子组件集成到uniApp中使用,只完成了简单的环境搭建,这篇文章讲解如何加载3D模型。 1 加入模型加载标签 在XR-Frame框架中,加载资源都是在wxml文件的标签中实现的。下面是wxml中完整的代码 index.wxml &l…

java Web线上网游商品交易平台用eclipse定制开发mysql数据库BS模式java编程jdbc

一、源码特点 jsp线上网游商品交易平台是一套完善的web设计系统,对理解JSP java SERLVET mvc编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用B/S模式开发。开发环境为TOMCAT7.0,eclipse开发,数据库为Mysql5.0…

React Native 应用打包

引言 在将React Native应用上架至App Store时,除了通常的上架流程外,还需考虑一些额外的优化策略。本文将介绍如何通过配置App Transport Security、Release Scheme和启动屏优化技巧来提升React Native应用的上架质量和用户体验。 配置 App Transport…

Linux文件和文件夹操作

一、文件操作 功能项命令实例作用文件创建vi /opt/learn/hello.txt 在目录/opt/learn下创建文件hello.txt并进入vi编辑界面 touch /opt/learn/test在目录/opt/learn下创建空白文件testcat > /opt/catfile创建文件catfile并在屏幕上输入内容,最后按 Ctrl D 退出…

【Node.js】WebSockets

概述 WebSockets是一种在浏览器和服务器之间建立持久连接的协议,它允许服务器主动推送数据给客户端,并且在客户端和服务器之间实现双向通信。 建立连接:客户端通过在JavaScript代码中使用WebSocket对象来建立WebSockets连接。例如&#xff1…

如何定位预防死锁

如何定位&预防死锁 什么是死锁? 简单来说就是并发环境下,两个或两个以上的线程互相等待资源,导致“永久阻塞”的现象 代码示例: public class Main {private static Object resource1 new Object();private static Objec…

达梦数据库自动备份(全库)+还原(全库) 控制台

一 前提 1.安装达梦数据库DB8(请参照以前文章) 我的数据库安装目录是 /app/dmDB8 2.已创建实例 (请参照上一篇文章) 二 准备测试数据 三 自动备份步骤 1.开启归档模式 开启DM管理工具管理控制台 弹不出来工具的 输入命令 xhost 第一步 将服务器转换为配置状态 右键-&g…

Kibana的安装(Linux版)

Kibana是一个针对Elasticsearch的开源分析及可视化平台,用来搜索、查看交互存储在Elasticsearch索引中的数据。使用Kibana,可以通过各种图表进行高级数据分析及展示。 Kibana让海量数据更容易理解。它操作简单,基于浏览器的用户界面可以快速创…

汽车电子行业知识:智能汽车电子架构

文章目录 3.智能汽车电子架构3.1.汽车电子概念及发展3.2.汽车电子架构类型3.2.1.博世汽车电子架构3.2.2.联合电子未来汽车电子架构3.2.3.安波福汽车电子架构3.2.4.丰田汽车电子架构3.2.5.华为汽车电子架构 3.智能汽车电子架构 3.1.汽车电子概念及发展 汽车电子是车体汽车电子…

阿里云对象存储OSS入门

阅读目录 一、阿里云OSS的使用 1、OSS是什么?2、OSS的使用 二、阿里云OSS的使用三、图床的搭建四:图床绑定阿里云OSS 编写不易,如果我的文章对你有帮助的话,麻烦小伙伴还帮忙点个赞再走! 如果有小伙伴觉得写的啰嗦&am…

基于SIR模型的疫情发展趋势预测算法matlab仿真

目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.本算法原理 5.完整程序 1.程序功能描述 基于SIR模型的疫情发展趋势预测算法.对病例增长进行SIR模型拟合分析,并采用模型参数拟合结果对疫情防控力度进行比较。整体思路为采用SIR微分方程模型…

PC电脑技巧[笔记本通过网线访问设备CMW500]

笔记本局域网访问设备 现在我有一台CMW500,我要用笔记本去访问它,但是我发现没有路由器就是不能够访问,通过网线连接设备就是ping不通: 这里设置TCP/IPv4的IP地址如下,这时候就可以pin通了:

Aspose.PDF功能演示:在 JavaScript 中合并两个 PDF 文件

在 Web 应用程序的世界中,处理和操作文档是一项常见的要求。当谈到 PDF 文件时,开发人员经常发现自己需要将 PDF 合并为单个 PDF 文件。因此,在这篇博文中,我们将探索如何使用强大的 PDF 库在 JavaScript 中轻松合并两个 PDF 文件…

【学习】软件测试行业 ,有哪些以就业为主的学习侧重点

今天给所有入行软测的同学们,帮大家梳理下以就业为主的学习侧重点,简单来说就是【这些都是重点,圈起来,要考的】,有需要的小伙伴可以往下看。 建议一:一定要学习一门编程语言,再开始使用自动化测…

Qt Design Studio 软件怎么用(详细+通俗+有趣)

建议:本文长期更新,建议点赞/收藏! 1. 啥是Qt Design Studio? Qt Design Studio 是一个用于设计和开发用户界面的工具,特别适合开发跨平台应用程序。它结合了UI设计和开发的工作流程,使得设计师和开发者可…

[WTL/Win32]_[初级]_[如何设置ListView的列宽不出现水平滚动条]

场景 开发WTL/Win32的程序时,经常会用到表格控件CListViewCtrl。这个控件需要设置列的宽度,当用完100%的宽度来平均分配给列宽时,一加载数据多,就会出现垂直滚动条后,水平滚动条也会同时出现的问题。怎么设置才能让水…

搭建 Apple Mac M1 stm32 开发环境

近期想学习 stm32 开发,看了些书和视频,买了开发板。开发板到了后就迫不及待的的进行尝试。由于我目前使用的电脑是 Apple M1 Pro,目前用的比较多的是 windows + keil。我先是在 mac 使用虚拟机,安装 win 环境来使用,但是我分别使用了 VMware 和 parallels desktop ,keil…

vue3:通过【自定义指令】实现自定义的不同样式的tooltip

一、效果展示 vue3自定义不同样式的tooltip 二、实现思路 1.ts文件 在ts文件中创建一个全局容器 import一个容器组件,用于存放自定义的各式组件 创建一个指令并获取到指令传递的数据,并为容器组件传值 2.容器组件 用于存放自定义Tooltip样式的组件…

VGG16神经网络搭建

一、定义提取特征网络结构 将要实现的神经网络参数存放在列表中,方便使用。 数字代表卷积核的个数,字符代表池化层的结构 cfgs {"vgg11": [64, M, 128, M, 256, 256, M, 512, 512, M, 512, 512, M],VGG13: [64, 64, M, 128, 128, M, 256, …