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

news2024/9/21 10:58:25

在这里插入图片描述

文章目录

  • 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.coloradmin.cn/o/1546297.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第九届蓝桥杯大赛个人赛省赛(软件类)真题C 语言 A 组-分数

solution1 直观上的分数处理 #include <iostream> using namespace std; int main() {printf("1048575/524288");return 0; }#include<stdio.h> #include<math.h> typedef long long ll; struct fraction{ll up, down; }; ll gcd(ll a, ll b){if…

后端常见面经之MySQL

MySQL字段类型 数值类型 整型经常被用到&#xff0c;比如 tinyint、int、bigint 。默认是有符号的&#xff0c;若只需存储无符号值&#xff0c;可增加 unsigned 属性。 int(M)中的 M 代表最大显示宽度&#xff0c;并不是说 int(1) 就不能存储数值10了&#xff0c;不管设定了显…

openssl AF_ALG引擎使用

cmd AF_ALG是Linux提供的一种虚拟接口&#xff0c;用于访问内核中的加密算法。在Linux中&#xff0c;可以使用AF_ALG接口配合加密算法框架&#xff08;Crypto API&#xff09;来进行加密操作。 以下是一个使用AF_ALG和openssl进行加密操作的例子&#xff1a; # 加密 openssl…

SpringBoot—@ConditionalOnBean与@ConditionalOnClass

一、ConditionalOnBean概念 需求场景 比如下面一种场景&#xff0c;我在实例化People对象的时候&#xff0c;需要注入一个City对象。这个时候问题来了&#xff0c;如果city没有实例化&#xff0c;那么下面就会报空指针或者直接报错。 所以这里需求很简单&#xff0c;就是当前c…

服务运营|香港大学雷骁:收益管理中价格歧视的公平性

编者按&#xff1a; INFORMS George B. Dantzig Dissertation Award 用于表彰运筹学和管理科学领域中具有创新性和实用性的最佳毕业设计。香港大学助理教授雷骁题为“Revenue Management in Video Games and With Fairness” 是这一奖项2023年度的提名者之一。 这篇毕业设计重…

利用AI技术预测未被充分监测的流域中的极端洪水事件笔记

利用人工智能&#xff08;AI&#xff09;技术预测未被充分监测的流域&#xff08;ungauged watersheds&#xff09;中的极端洪水事件 文章目录 利用人工智能&#xff08;AI&#xff09;技术预测未被充分监测的流域&#xff08;ungauged watersheds&#xff09;中的极端洪水事件…

bsd猜想 Murmuration of Eliptic Curves(笔记)

BSD Alexey Pozdnyakov (University of Connecticut) YUTUBE视频&#xff0c; B站搬运地址新生代女数学家Nina Zubrilina得到椭圆曲线椋鸟群飞模式精确公式与证明 Arithmetic Geometry算术几何 希尔伯特第十问题 希尔伯特第十问题&#xff08;Hilbert’s Tenth Problem&#…

TransUNet论文笔记

论文&#xff1a;TransUNet&#xff1a;Transformers Make Strong Encoders for Medical Image Segmentation 目录 Abstract Introduction Related Works 各种研究试图将自注意机制集成到CNN中。 Transformer Method Transformer as Encoder 图像序列化 Patch Embed…

拼板注意事项和步骤

拼板注意事项和步骤 综述&#xff1a;本文讲述了AD软件中拼板的注意事项以及拼板的步骤。 1. 拼板注意事项 拼板时应注意定位孔、光学定位点、工艺边、间距和器件冲突等问题。 2. 拼板步骤 ①点击“放置”→“拼板阵列”→“panels”→“properties”&#xff0c;浏览需要…

晴问算法 动态规划(简单)

动态规划的递归写法和递推写法 斐波那契数列II 题目描述 给定正整数&#xfffd;&#xff0c;求斐波那契数列的第&#xfffd;项&#xfffd;(&#xfffd;)。 令&#xfffd;(&#xfffd;)表示斐波那契数列的第&#xfffd;项&#xff0c;它的定义是&#xff1a; 当&…

【MD】激光驱动原子动力学的全尺寸从头算模拟

Zeng Q, Chen B, Zhang S, et al. Full-scale ab initio simulations of laser-driven atomistic dynamics[J]. npj Computational Materials, 2023, 9(1): 213.核心研究内容&#xff1a; 本文研究了激光驱动的原子动力学的全尺度从头算模拟。研究的重点是探讨在极端条件下材料…

C语言结构体之位段

位段&#xff08;节约内存&#xff09;&#xff0c;和王者段位联想记忆 位段是为了节约内存的。刚好和结构体相反。 那么什么是位段呢&#xff1f;我们现引入情景&#xff1a;我么如果要记录一个人是男是女&#xff0c;用数字0 1表示。我们发现只要一个bit内存就可以完成我们想…

讯优随身WiFi可靠吗?讯优和格行随身WiFi哪个好?随身wifi推荐测评!随身WiFi哪个牌子比较好用?随身WiFi怎么选?随身WiFi推荐第一名!

随身WiFi挑花眼&#xff1f;不知道哪款好用&#xff1f;今天&#xff0c;我将为大家带来迅优与格行两大随身WiFi品牌的真实测评。 价格方面&#xff1a;迅优随身WiFi以价格优惠为卖点&#xff0c;吸引了众多消费者的目光。对于预算有限的用户来说&#xff0c;迅优确实提供了一个…

刷题记录:最长公共前缀

编写一个函数来查找字符串数组中的最长公共前缀。 如果不存在公共前缀&#xff0c;返回空字符串 ""。 示例 1&#xff1a; 输入&#xff1a;strs ["flower","flow","flight"] 输出&#xff1a;"fl"示例 2&#xff1a; 输…

治疗植物神经功能紊乱的关键竟是它!

植物神经紊乱&#xff0c;是指交感神经系统和副交感神经系统之间的平衡失调&#xff0c;导致心率不规律、胃肠问题、焦虑、失眠等一系列症状。长期植物神经紊乱可能导致更严重的心血管疾病、消化系统问题以及心理健康隐患。它可能源于饮食结构失衡、物理环境压力过大、心理压力…

Xinstall:用智能数据传递,打造高效的App推广体系

随着移动互联网的快速发展&#xff0c;App已经成为人们日常生活中不可或缺的一部分。然而&#xff0c;对于开发者来说&#xff0c;如何有效地推广App并衡量推广效果却是一个巨大的挑战。这时&#xff0c;Xinstall作为一站式App全渠道统计服务商&#xff0c;提供了强有力的解决方…

模板(一)

文章目录 模板&#xff08;一&#xff09;1.泛型编程2.函数模板2.1函数模板概念2.2函数模板格式2.3函数模板的原理2.4函数模板的实例化2.4.1隐式实例化2.4.2显式实例化 2.5模板参数的匹配原则 3.类模板3.1类模板的定义格式3.2类模板的实例化 模板&#xff08;一&#xff09; 1…

25.7 MySQL 数据库和表的基本操作

1. 基础知识 1.1 一条数据的存储过程 存储数据确实是处理数据的基石, 只有确保数据被准确无误且有条理地存储, 我们才能对其进行深入的处理和细致的分析. 否则, 这些数据就像是一团毫无章法的乱麻, 让我们难以捉摸其内在的逻辑和价值.那么, 如何才能够将用户那些与经营紧密相关…

人工智能时代如何高效完成营销内容计划

智能对话升级&#xff01;【Kompas AI】AI对话助手&#xff0c;让沟通更高效 在人工智能时代&#xff0c;要高效完成营销计划&#xff0c;我们可以利用人工智能的多种能力来增强营销策略的精准度和执行效率。借助人工智能的力量&#xff0c;企业不仅可以提高营销计划的执行效率…

Rust高级爬虫:如何利用Rust抓取精美图片

引言 在当今信息爆炸的时代&#xff0c;互联网上的图片资源丰富多彩&#xff0c;而利用爬虫技术获取这些图片已成为许多开发者的关注焦点。本文将介绍如何利用Rust语言进行高级爬虫编程&#xff0c;从而掌握抓取精美图片的关键技术要点。 Rust爬虫框架介绍 Rust语言生态中有…