22-10-14 西安 spring循环依赖、对象内存布局、synchronized锁升级

关于锁升级参考了周阳老师在b站的juc视频,阳哥讲的很好

尚硅谷2022版JUC并发编程(对标阿里P6-P7)_哔哩哔哩_bilibili

spring循环依赖

1、循环依赖问题

什么是循环依赖

默认单例模式

默认单例是不存在问题的,这也是我们要接下来要研究的,spring是怎么解决循环依赖问题的

 在applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="a" class="com.atguigu.spring.pojo.A">
        <property name="b" ref="b"></property>
    </bean>
    <bean id="b" class="com.atguigu.spring.pojo.B">
        <property name="a" ref="a"></property>
    </bean>
</beans>

测试从容器中获取bean

 @Test
 public void test1() {
     ClassPathXmlApplicationContext applicationContext =
             new ClassPathXmlApplicationContext("applicationContext.xml");
     A a = (A) applicationContext.getBean("a");
     B b = (B) applicationContext.getBean("b");
     System.out.println(a);
     System.out.println(b);
 }

控制台打印

原型模式

bean作用域改为原型,如下scope=prototype

对于prototype作用域bean,spring容器无法完成依赖注入

因为spring容器不进行缓存prototype作用域的bean,因此无法提前暴露一个正在创建中的bean。报错如下:

官方:使用构造器注入,可能会造成无法解决的循环依赖,推荐使用setter注入

Core Technologies


2、Spring三级缓存

spring通过三级缓存提前暴露对象来解决循环依赖

package org.springframework.beans.factory.support;
...
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
    ...
    /** 
    单例对象的缓存:bean名称—bean实例,即:所谓的单例池。
    表示已经经历了完整生命周期的Bean对象
    第一级缓存
    */
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
    /**
    早期的单例对象的高速缓存: bean名称—bean实例。
    表示 Bean的生命周期还没走完(Bean的属性还未填充)就把这个 Bean存入该缓存中也就是实例化但未初始化的 bean放入该缓存里
    第二级缓存
    */
    private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
    /**
    单例工厂的高速缓存:bean名称—ObjectFactory
    表示存放生成 bean的工厂
    第三级缓存
    */
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
}

对象名 含义
一级缓存 singletonObjects 存放已经经历了完整生命周期的Bean对象
二级缓存 earlySingletonObjects 存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完)
三级缓存 singletonFactories 存放可以生成Bean的工厂

一级缓存里存的是成品对象,实例化和初始化都完成了


3、四个方法

 4个方法的使用

  • getsingleton()---从缓存中获取单例对象,默认会先去一级缓存获取值
  • doCreateBean()---创建一个bean实例
  • populateBean()--初始化bean实例
  • addsingleton()--将单例对象加入缓存中

4、spring源码粗看

a和b的创建都在断点这一行呢

去创建a

这个方法一旦执行完毕,a就创建成功了

创建出来的a,存在哪,存三级缓存 

 但是a还没有初始化,是个半成品

找b,去创建b

创建完成之后又把b也放到3级缓存。

这个时候,a,b都在三级缓存。

再对b初始化

要对b初始化,就要拿到a,拿到后把a放到2级缓存,同时把3级缓存的a移除。真正的给b对象的a属性赋值(深拷贝)

 初始化完成后,再把b放到一级缓存

此时a还在2级缓存,b直接从三级缓存跳到了一级缓存

b放入一级缓存后,b创建结束

再继续给a对象的b属性赋值

把a放到一级缓存,2,3级缓存的a移除

此时a,b都到了一级缓存

结束a的创建

========补充

深拷贝和浅拷贝的区别

一个对象中存在俩种类型的属性,基本数据类型和实例对象的引用

浅拷贝:

只会拷贝基本数据类型的值,以及实例对象的引用地址,浅拷贝出来的对象内部的类属性指向的是同一个对象

深拷贝:

即会拷贝基本数据类型的值,也会针对实例对象的引用地址指向的对象进行复制,深拷贝出来的对象,
内部的属性指向的不是同一个对象

5、循环依赖总结

1,Spring创建 bean主要分为两个步骤,创建原始bean对象,接着去填充对象属性和初始化

2,每次创建 bean之前,我们都会从缓存中查下有没有该bean,因为是单例,只能有一个。

3,当创建 A的原始对象后,并把它放到三级缓存中,接下来就该填充对象属性了,这时候发现依赖了B,接着就又去创建B,同样的流程,创建完B填充属性时又发现它依赖了A又是同样的流程,不同的是:这时候可以在三级缓存中查到刚放进去的原始对象A。

所以不需要继续创建,用它注入 B,完成 B的创建既然 B创建好了,所以 A就可以完成填充属性的步骤了,接着执行剩下的逻辑,闭环完成

Spring解决循环依赖依靠的是Bean的"中间态"这个概念,而这个中间态指的是已经实例化但还没初始化的状态—>半成品。实例化的过程又是通过构造器创建的,如果A还没创建好出来怎么可能提前曝光,所以构造器的循环依赖无法解决


6、循环依赖衍生问题

问题1:为什么构造器注入属性无法解决循环依赖问题?

由于spring中的bean的创建过程为先实例化 再初始化(在进行对象实例化的过程中不必赋值)将实例化好的对象暴露出去,供其他对象调用,然而使用构造器注入,必须要使用构造器完成对象的初始化的操作,就会陷入死循环的状态

问题2:一级缓存能不能解决循环依赖问题? 不能

在三个级别的缓存中存储的对象是有区别的 一级缓存为完全实例化且初始化的对象 二级缓存实例化但未初始化对象 如果只有一级缓存,如果是并发操作下,就有可能取到实例化但未初始化的对象,就会出现问题

问题3:二级缓存能不能解决循环依赖问题?

理论上二级缓存可以解决循环依赖问题,但是需要注意,为什么需要在三级缓存中存储匿名内部类(ObjectFactory),原因在于需要创建代理对象 

eg:现有A类,需要生成代理对象 A是否需要进行实例化(需要) 在三级缓存中存放的是生成具体对象的一个匿名内部类,该类可能是代理类也可能是普通的对象,而使用三级缓存可以保证无论是否需要是代理对象,都可以保证使用的是同一个对象,而不会出现,一会儿使用普通bean 一会儿使用代理类


对象内存布局

1、对象内存布局

Object o = new Object()//会占用堆内存多少个字节

在HotSpot虛拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header) 、实例数据(Instance Data)和对齐填充( Padding)。

对齐填充,保证对象大小是8个字节的倍数,具体后面说

因为虚拟机规定:对象起始地址必须是8字节的整数倍。
填充数据不是必须存在的,仅仅是为了字节对齐,保证对象大小是8个字节的倍数

实例数据:存放类的属性(Field)数据信息,包括父类的属性信息

对象头又有2部分构成:

  1. 对象标记 Mark Word
  2. 类元信息(类型指针)klass pointer

如果是数组在对象头会多一个部分表示数组的长度

对象头中的对象标记Mark Word中有什么

类元信息(类型指针klass pointer)存储的是指向该对象类元数据(kass)的首地址。

对象头中的类型指针指向方法区中的klass类元数据,JVM通过这个类型指针确定对象是哪个类的实例

MarkWord被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据 它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着锁标志位的变化而变化。


2、JOL [Java Object LayOut]

JOL用来分析对象在jvm的大小和布局

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

随便定义个类,主方法中输出以下内容

 public static void main(String[] args) {
     //VM的细节详细情况
     System.out.println(VM.current().details());
     //证明:所有的对象分配的字节都是8的整数倍
     System.out.println(VM.current().objectAlignment()
 }


synchronized锁升级

synchronized升级原理

把之前重量级锁变成在一定条件下使用偏向锁以及使用轻量级(自旋锁CAS)的形式,而不是无论什么情况都使用重量级锁

1、Mark Word中锁标志位

用锁能够实现数据的安全性,但是会带来性能下降。无锁能够基于线程并行提升程序性能,但是会带来安全性下降。

Java6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁

java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,
需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,

对象头中的Mark Word根据锁标志位的不同而被复用及锁升级策略


2、Mark Word中锁指向

在64位系统中,Mark Word占了8个字节(64bit),类型指针占了8个字节,一共是16个字节

对象年龄占了4个bit,所以最大是1111,故分代年龄最大就是15,然后从新生代进入老年代。

cms_free垃圾标识

锁指向

  • 偏向锁:MarkWord存储的是偏向的线程ID;
  • 轻量锁:MarkWord存储的是指向线程栈中Lock Record的指针
  • 重量锁:MarkWord存储的是指向堆中的monitor对象的指针

3、无锁

无锁:初始状态,一个对象被实例化后,如果还没有被任何线程竞争锁,那么它就为无锁状态(001)

 new 一个对象,演示无锁状态的markword中各种标志位

public static void main(String[] args) {
    Object o = new Object();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

控制台打印:倒着看,从右下角到左上角看,所以看到是001,无锁

31位hashcode怎么体现?

调用hashcode才会有该值,否则无

public static void main(String[] args) {
    Object o = new Object();
    System.out.println("10进制:"+o.hashCode());
    System.out.println("16进制:"+Integer.toHexString(o.hashCode()));
    System.out.println("2进制:"+(Integer.toBinaryString(o.hashCode())));
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

不管是看二进制还是16进制,我们都看得出来要从下往上,从右往左看。每一个


4、偏向锁

经过以往的研究发现⼤多数情况下锁不仅不存在多线程竞争,⽽且总是由 同⼀线程 多次获得,于是引⼊了偏向锁。

偏向锁:java允许cpu偏向某一个线程,让它一直执行,而不是用户态和内核态的切换

偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也即偏向锁在资源没有竞争情况下消除了同步语句,直接提高程序性能

当对象的锁第一次被某个线程持有的时候,对象头Mark Word会记录下偏向线程ID。

这样偏向线程就一直持有着锁,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接会去检查锁的MarkWord里面是不是放的自己的线程ID。

注意:线程执行完并不会主动释放偏向锁,下次来的时候也就不需要重新加锁了
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁

用锁的线程只有一个,偏向锁几乎没有额外开销,性能极高

偏向锁实现原理

⼀个线程在第⼀次进⼊同步块时,会在对象头和栈帧中的锁记录⾥存储锁的偏向的
线程 ID 。当下次该线程进⼊这个同步块时,会去检查锁的 Mark Word ⾥⾯是不是放
的⾃⼰的线程 ID
如果是,表明该线程已经获得了锁,以后该线程在进⼊和退出同步块时不需要花费CAS操作来加锁和解锁 ;如果不是,就代表有另⼀个线程来竞争这个偏向锁。这个时候会尝试使⽤CAS 来替换 Mark Word ⾥⾯的线程 ID 为新线程的 ID ,这个时候要分两种情况:
成功,表示之前的线程不存在了, Mark Word ⾥⾯的线程 ID 为新线程的 ID ,锁不会升级,仍然为偏向锁;
失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为
0 ,并设置锁标志位为 00 ,升级为轻量级锁,会按照轻量级锁的⽅式进⾏竞争
锁。

5、轻量级锁

多个线程在 不同时段 获取同⼀把锁,即不存在锁竞争的情况,也就没有线程阻塞。针对这种情况,JVM 采⽤轻量级锁来避免线程的阻塞与唤醒。

有线程来参与锁的竞争,但是获取锁的冲突时间极短,轻量级锁本质是自旋锁CAS。轻量级锁是为了在线程近乎交替执行同步块时提高性能,并不会去阻塞线程

锁怎么升级为轻量级锁


6、重量级锁

有大量的线程参数锁的竞争,冲突性很高。重量级锁会阻塞线程


7、锁升级之后,hashcode去哪了

锁升级为轻量级或重量级锁后,Mark Word中保存的分别是线程栈帧里的锁记录指针和重量级锁指针,已经没有位置再保存哈希码、GC分代年龄了,那么这些信息被移动到哪里去了呢?


8、锁消除、锁粗化

锁消除

从JIT角度看相当于无视它,synchronized(o)不存在了, 这个锁对象并没有被共用扩散到其它线程使用,极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用

这么写无意义。表面上语法没问题,但是JIT会无视 

锁粗化

假如方法中首尾相接,前后相邻的都是同一个锁对象,那么JIT编译器就会把这几个synchronized块合并成一个大块,加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能

JIT编译器锁粗话后


9、三种锁对比

猜你喜欢

转载自blog.csdn.net/m0_56799642/article/details/127323969
今日推荐