前置需要了解的知识:
成员变量需要考虑线程安全问题,局部变量不需要考虑线程安全问题。
Spring bean并没有可变状态(例如Service类与Dao类),所以在某种程度上来说Spring的单例bean是线程安全的。
无可变状态的类(无状态的类):判断当前的成员变量能不能被修改,如果不能被修改,则称之为无状态的类,这种无状态的成员变量无线程安全问题。
总结:
Spring框架中的单例bean不是线程安全的!
Spring框架中有一个@Scope注解,默认值是singleton,是单例的。因为一般在spring的bean都是注入无状态的对象,并不会产生线程安全问题,如果在bean中定义了可修改的成员变量,需要考虑线程安全问题,可以使用多例或者加锁来解决。
对面试官的回答:
Spring框架中的单例bean不是线程安全的。
当出现多个用户同时请求一个服务的场景,容器会给每一个请求分配一个线程,多线程会并发的执行该请求对应的业务逻辑(成员方法),如果业务逻辑中有对该单例bean状态的修改(对该单例bean的成员属性进行修改)
,则必须考虑线程同步问题。
Spring框架并没有对单例bean进行任何的多线程封装处理,关于单例bean的线程安全问题和并发问题需要我们开发者自己去搞定。
比如:我们通常在项目中使用的Spring bean都是不可变状态的(例如Service类与Dao类),所以在某种程度上说Spring的单例bean是线程安全的。
但是如果你的bean是有多种状态(属性可以被修改)的话,就需要自己去保证线程的安全。比较简单的办法就是将单例bean变成多利bean,将@Scope(“”)注解中的值从singleton变更为prototype。
补充:线程安全与并发问题的异同?
并发问题是指多个线程同时访问共享资源时可能出现的问题,例如死锁、竞态条件等。并发问题通常需要使用同步机制来解决,例如锁、信号量等。
线程安全问题是指在多线程环境下,对共享资源的访问不会导致数据不一致或程序崩溃等问题。线程安全问题通常需要使用线程安全的数据结构或同步机制来解决,例如使用线程安全的集合类、使用 synchronized 关键字等。
因此,可以说并发问题是线程安全问题的一种表现形式,但并不是所有的线程安全问题都是并发问题。
什么是AOP?
AOP称为面向切面编程,他主要的应用场景是将哪些与业务无关,但却对多个对象产生影响的公共行为和逻辑
(例如事务,日志等),抽取并封装为一个可重用的模块,这个模块被命名为切面(Aspect)
,减少系统中的重复代码,降低模块间的耦合度,提高系统的可维护性。
spring事务的底层用到了AOP进行实现,而AOP的底层用到了动态代理去实现。
你们项目中有没有用到AOP?
常见的AOP使用场景:
记录操作日志实现核心
:使用aop中的环绕通知+切点表达式(找到要记录日志的方法),通过环绕通知的参数获取请求方法的参数(类、方法、注解、请求方式等),获取到这些参数以后,保存到数据库中
Spring中的事务是如何实现的?
其本质是通过AOP功能,对方法前后进行拦截,在执行方法之前开启事务,在执行完目标方法之后,根据执行情况提交或者回滚;
场景一:异常捕获处理
原因:
事务通知只有捉到了目标抛出的异常,才能进行后续的回滚处理,如果目标自己处理掉了异常,则spring的事务无法洞悉,也就没有进行回滚。
解决方案:
在catch块中手动将异常抛出,让spring的 事务能够察觉到即可完成在出现异常是进行事务回滚。
场景二:抛出检查异常
失效的原因
:spring默认只会回滚非检查异常(Runtime异常),上方截图代码中抛出的是一个检查异常,因此事务失效;
解决方案
:配置rollbackFor属性
@Transaction(rollbackFor = Exception.class)
该配置描述:只要是异常都会进行回滚;
场景三:非public方法导致事务失效
事务失效原因
:Spring为方法创建代理,添加事务通知,前提条件是该方法是public的;
解决方案:
将方法改为public的即可;
前置需要了解的知识
BeanDefinition:
Spring容器在实例化时,会将xml配置的的信息封装成BeanDefinition对象,Spring根据BeanDefinition来创建bean,该对象中有很多的属性来描述bean;
前置需要了解的知识
循环引用出现的场景:
产生循坏依赖的原因,与spring中bean的生命周期有关,直接上图理解:
为了帮助记忆,你可以理解为三个人在搞对象,三角恋,A喜欢B,心里只有B,而B心里只有C,C心里只有A,他们都是非心里那个它不娶,导致三人最后一起在养老院里玩耍
缓存名称 | 源码名称 | 作用 |
---|---|---|
一级缓存 | singletonObjects | 单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象 |
二级缓存 | earlySingletonObjects | 缓存早期的bean对象,此时的bean生命周期还没有走完 |
三级缓存 | singletonFactories | 缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的 |
三级缓存解决循环依赖:
一级缓存的作用:限制bean在beanFactory中只存一份,即实现singleton scope,它解决不了循环依赖;
打破循环依赖,需要一级缓存与二级缓存同时使用:
二级缓存充当了一个中间人的存在(暂存区)
解决循环依赖的过程描述:
1.首先实例化A对象,此时会得到原始的实例对象A(当前的A对象是一个半成品对象)
2.将半成品对象A存入到二级缓存(earlySingletonObjects )中
3.此时A对象需要注入B对象,B对象此刻还不存在
4.实例化B,对象B的半成品也存入到二级缓存当中,此时二级缓存中有:对象A与对象B的半成品
5.此时对象B需要注入对象A,而对象A还没有初始化完成,单可以从二级缓存中拿出半成品对象A注入到对象B中,此时对象B就可以创建成功,将初始化后的对象存入单例池(一级缓存)中,并清除二级缓存中的半成品对象B;
6.正在初始化的对象A也能够从单例池获取B对象从而创建成功,并将创建成功的对象A存入到一级缓存中,并清除二级缓存中的半成品对象A;
注意
:一二级缓存只能解决非代理对象的循环依赖问题,对于代理对象的循环依赖问题需要三级缓存才能解决。
代理类出现循环依赖需要三级缓存才能解决:
1.实例化类A,会得到一个原始对象A,原始对象A生成一个ObjectFactory对象:
A-ObjectFactory
,并放入到三级缓存中;
2.此时对象A需要注入对象B,对象B还未存在;
3.实例化B获得原始对象B,用原始对象B生成一个ObjectFactory对象:B-ObjectFactory
,并放入到三级缓存中;
4.对象B此时需要注入对象A,但是对象A未初始化完,因此需要去三级缓存中获取A-ObjectFactory,通过A-ObjectFactory去生成对象A的代理(或普通的对象A)
,放入二级缓存中(此时生成的对象A的代理是一个半成品),对象B从二级缓存中拿出对象A的代理进行注入;
5.对象B完成初始化,存入到一级缓存中;
6.此时对象A可以注入对象B了,对象A(代理对象)也初始化成功,并放入一级缓存中
三级缓存解决产生循环依赖问题是在bean的初始化阶段的循环依赖问题 ,而无法结局bean生成时的循环依赖问题:
通过构造器注入依赖出现的问题,通过三级缓存解决不了,因此需要手动解决:
面试时的回答
spring中bean的循环引用
(然后复述三级缓存解决循环依赖的流程,最好俩都说,记不起来就挑想得起来的说)
构造方法出现了循环依赖怎么解决?
原因:由于bean的生命周期中构造函数是第一个执行的,spring框架并不能解决构造函数的依赖注入
解决方案: 使用@Lazy进行懒加载,什么时候需要对象再进行bean的创建
注解 | 说明 |
---|---|
@Component、@Controller、@Service、@Repository | 使用在类上用于实例化bean |
@Autowired | 使用在字段上用于根据类型依赖注入 |
Qualifier | 结合@Autowired一起使用,用于根据名称进行依赖注入 |
@Scop | 标注Bean的作用范围 |
@Configuration | 指定当前类是一个配置类,当创建容器时会从该类上加载注解 |
@ComponentScan | 用于指定在初始化容器时要扫描的包 |
@Bean | 使用在方法上,标注将该方法的返回值存储到Spring容器中 |
@Import | 使用@Import导入的类会被Spring加载到IOC容器中 |
@Aspect、@Before、@After、@Around、@Poincut | 用于切面编程(AOP) |