spring 面试题
单例bean是单例模式吗?
是;单例bean不意味着一个jvm中只存在一个bean,使用@Bean注解可以注入名字不同的相同类型的bean对象
不是指在Spring容器中只有一个userService类型的对象,可以有多个此类型的对象。Spring找到对应的对象是先 byType 再 byName。
spring aop 是如何实现的?
spring AOP是利用动态代理机制,如果一个bean实现了接口,那么会采用jdk动态代理来生成接口的代理对象;如果一个bean没有实现接口那么会采用cglib来生成代理对象;代理对象的作用就是代理原本的bean对象,代理对象在执行某个方法时,会在该方法的基础上增加一些切面逻辑,使得我们可以利用aop来实现一些诸如登录校验,权限控制,日志记录等统一功能
Spring中的事务是如何实现的?
1.spring事务底层是基于数据库事务和aop机制的
2.首先对于使用了@Transactional注解的bean,spring会创建一个代理对象作为bean
3.当调用代理对象的方法时,会先判断该方法上是否加了@Transactional注解
4.如果加了,那么则利用事务管理器创建一个数据库连接
5.并且修改数据库连接的autocommit属性为false,禁止此连接的自动提交
6.然后执行当前方法,方法中会执行sql
7.执行完当前方法后,如果没有出现异常就直接提交事务
8.如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
9.spring的事务隔离级别对应数据库的隔离级别
10.spring事务传播机制就是spring事务自己实现的,也是spring事务中最复杂的
11.spring事务的传播机制是基于数据库连接来做的,一个数据库连接一个事务,如果传播机制配置为需要新开一个事务,那么实际上就是先建立一个数据库连接,在此新数据库连接上执行sql
spring事务分为:声明式和编程式
Spring事务传播机制
spring事务传播机制是指,包含多个事务的方法在相互调用时,事务是如何在这些方法中传播的。
spring事务传播机制的级别包含以下7种:
传播级别 | 描述 |
---|---|
REQUIRED(默认) | 如果当前存在事务,则加入该事务;如果没有事务,则创建一个新的事务 |
SUPPORTS | 如果当前存在事务,则加入该事务;如果没有事务,则以非事务方式执行 |
MANDATORY | 如果当前存在事务,则加入该事务;如果没有事务,则抛出异常 |
REQUIRES_NEW | 创建一个新的事务,如果当前存在事务,则将其挂起 |
NOT_SUPPORTED | 以非事务方式执行,如果当前存在事务,则将其挂起 |
NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常 |
NESTED | 如果当前存在事务,则在嵌套事务内执行,如果没有事务,则创建一个新的事务 |
事务传播
- 支持当前事务:REQUIRED,SUPPORTS,MANDATORY
- 不支持当前事务:REQUIRED_NEW,NOT_SUPPORTED,NEVER
- 嵌套事务:NESTED
REQUIRED,REQUIRED_NEW和NESTED的区别
A 方法为外层方法, B方法为内层方法
REQUIRED: A和B只要异常,且异常未被捕获,则A和B都会回滚
REQUIRED_NEW:B出现异常,B会回滚,如果A捕获了B的异常则不会回滚,如果未捕获则一起回滚;如果B正常,A出现异常,则B会正常提交,A会回滚;
NESTED:B 出现异常,B会回滚,如果A捕获了B的异常则正常提交,如果未捕获,则AB一起回滚;如果B正常,A出现异常,则AB一起回滚
Spring事务隔离级别
- DEFAULT: 数据库使用的默认隔离级别
- READ_UNCOMMITTED: 允许读取未提交的数据,可能导致脏读,不可重复读和幻读
- READ_COMMITTED: 只能读取以提交的数据,可以防止脏读,但是不可避免不可重复读和幻读
- REPEATABLE_READ: 保证同一事务内多次读取同一数据时结果一致,可以防止脏读和不可重复读,但不能避免幻读
- SERIALIZABLE: 最高的隔离级别,通过强制事务串行执行来避免脏读,不可重复读和幻读
脏读:一个事务读取到了另外一个事务修改的数据之后,后一个事务又进行了回滚操作,从而导致第一个事务读取的数据是错误的
不可重复读:一个事务两次查询得到的结果不同,因为在两次查询中间,有另外一个事务把数据修改了;主要针对update操作
幻读:一个事务两次查询结果中得到的结果集不同,因为在两次查询中另一个事务新增了一部分数据;主要针对insert和delete操作
mvcc主要针对的 RC和RR级别
Spring事务失效场景
-
方法内的自调用:spring事务是基于aop的,只要使用代理对象调用某个方法时,spring事务才能生效,而在一个方法中调用使用this.xxx()调用方法时,this并不是代理对象,所以会使事务失效
解决办法
- 把调用方法拆分到另外一个Bean中
- 自己注入自己,使用代理对象调用该方法
- AopContext.currentProxy()+@EnableAspectJAutoProxy(exposeProxy = true)
-
方法是private的,Spring事务会基于GCLIB来进行aop。而cglib会基于父子类来生效,子类是代理类,父类是被代理类,如果父类中的某个方法是private的那么子类就没办法重写它,也就没有办法额外增加spring事务逻辑
-
方法是final的,原因和private一样,也是由于子类不能重写父类中的final方法
-
单独的线程调用方法:当mybatis或者jdbcTemplate执行sql时,会从ThreadLocal中去获取数据库连接对象,如果开启事务的线程和执行sql的线程是同一个,那么就能拿到数据库连接对象,如果不是同一个线程,那就拿不到数据库连接对象,这样Mybatis或jdbcTemplate就会自己去新建一个数据库连接用来执行sql,此数据库连接的autocommit为true,那么执行完sql就会提交,后续再抛异常也就不能再回滚之前已经提交的sql了
-
没有加@Configuration注解:如果用springboot基本上没有这个问题,但是如果用spring,那么可能会有这个问题,这个问题的原因其实也是由于mybatis或jdbcTemplate会从ThreadLocal中去获取数据库连接,但是ThreadLocal中存储的是一个MAP,MAP的key是DataSource对象,value为连接对象,而如果我们没有在Appconfig上添加@Configuration注解的话,会导致Map中存在的DataSource对象和Mybatis或JdbcTemplate中的DataSource对象不相等,从而也拿不到数据库连接,导致自己去创建数据库连接了
-
异常被吃掉:如果spring事务没有捕获到异常,那么也就不会回滚了,
-
类没有被spring管理
-
数据库不支持事务
Spring中的Bean创建的生命周期有哪些步骤
- 解析类得到BeanDefinition
- 如果存在多个构造方法,推断构造方法
- 实例化
- 填充属性,也就是依赖注入@Autowired
- 处理Aware回调,如:BeanNameAware,BeanFactoryAware
- 初始化前,处理@PostConstruct注解 beanPostProcessor
- 初始化,处理InitializingBean接口
- 初始化后,进行aop
- 如果当前是单例bean,则会把bean放到单例池singletonObjects中
- 使用bean
- spring容器关闭时,调用destroy()方法销毁
Spring支持bean的作用域?
作用域 | 说明 |
---|---|
singleton | 默认,每个容器中只有一个bean的实例,单例的模式由beanFactory来维护。该对象的生命周期是与spring ioc容易起一致 |
prototype | 为每一个bean请求提供一个实例,在每次注入时都会创建一个新的对象(原型模式) |
request | 在每个http请求中创建一个单例对象,也就是说再每个请求中都会服用这个单例对象 |
session | 作用范围和request类似;确保每个session中都有一个bean的实例,session过期后,bean会随之失效 |
global-session | 和Portlet应用相关 |
Spring ioc的工作流程
ioc的工作流程大致可以分为两个阶段:
ioc容器的初始化阶段,这个阶段主要是根据程序里面定义的xml或者注解方式声明的bean 通过解析加载后生成BeanDefinition,然后把BeanDefinition 注册到ioc容器中,beanDefinition包含类的一些定义和属性,最后把BeanDefinition对像放到一个map的容器中,从而完成ioc容器的初始化
bean的实例化和初始化阶段,这个阶段会对非懒加载的bean通过反射的方式进行初始化,然后完成bean的依赖注入;我们通常会使用@Autowired @Resource 或者getBean() 方式去ioc容器中获取实例;对于懒加载或者非单例bean ,一般是在获取bean的时候再进行加载
ApplicationContext和BeanFactory有什么区别
两者都是spring提供的ioc容器
BeanFactory是spring中非常核心的组件,表示bean工厂,可以生成bean;而ApplicationContext继承了BeanFactory,所以ApplicationContext拥有BeanFactory所有的特点,也是一个bean工厂;但是ApplicationContext还继承了EnvironmentCapable,MessageSource,ApplicationEventPublisher等接口,从而实现获取系统环境变量,国际化,事件发布等功能。
BeanFactory在调用getBean()方法的时候实例化bean,而ApplicationContext在容器启动的时候就实例化bean
Spring容器启动流程是怎么样的
- 在创建spring容器,也就是启动spring时:
- 首先会进行扫描,扫描得到所有的BeanDefinition对象,并放在一个Map中
- 然后筛选出非懒加载的单例BeanDefinition进行创建bean,对于多例bean不需要在启动过程中去进行创建,对于多例bean会在每次获取bean时利用BeanDefinition去创建
- 利用beanDefinition创建bean就是bean的创建生命周期
- 单例bean创建完了之后,spring会发布一个容器启动事件
- spring启动结束
@SpringBootApplication注解有什么用?
@SpringBootApplication是一个复合注解
@SpringBootApplication=@SpringBootConfiguration+ @EnableAutoConfiguration + @ComponentScan
@ComponentScan:从而Spring容器会进行扫描,扫描路径为当前在解析的这个类所在的包路径
@EnableAutoConfiguration:这个注解会负责进行自动配置类的导入,也就是将项目中的自动配置类导入到spring容器中,从而得到解析;等于@AutoConfigurationPackage + @Import(AutoConfigurationImportSelector.class)
@SpringBootConfiguration:相当于@Configuration 表示当前类是一个配置类
springboot 自动配置原理?
@SpringbootApplication 注解中包含@CompentScan会去扫描该类所在的包名将所有配置类加入到spring中;同时@EnableAutoConfiguration = @AutoConfigurationPackage+@Import(AutoConfigurationImportSelector.class)
AutoConfigurationImportSelector 实现了DeferredImportSelector接口,通过selectImports()方法会去调用loadSpringFactories()方法,首先从缓存中去查找bean对象,缓存中不存在则去MATE-INFO下面的spring.factories下EnableAutoConfiguration的字符串,包含以接口或者抽象类全类名为键,其实现类为值;回去加载所有的实现类
AutoConfigurationImportSelector的实现流程: selectImports()方法通过springFactories机制加载配置文件(通过classloader去获取classpath中的配置文件META-INFO/spring.factories);找出所有的自动配置类,即以EnableAutoConfiguration.class为key的,符合条件的配置类;根据注解@Conditional过滤掉不必要的自动配置类
SpringBoot中Spring.factories文件有什么作用?
spring.factories是SpringBoot SPI机制实现的核心,SPI机制表示扩展机制,所以spring.factories文件的作用就是用来对SpringBoot进行扩展的。比如我们可以通过该文件去添加ApplicationListener等,spring会把这些加载到spring容器中
SpringBoot 实现热部署有哪几种方式?
- Spring Loaded
- Spring-boot-devtools
SpringMVC处理请求的流程?
- 客户端请求发送给DispatcherServlet
- DispatcherServlet将请求发送给HandlerMapping
- HandlerMapping返回dispatcherServlet一个handler
- dispatcherServlet将handler发送给handlerAdapter
- handlerAdapter返回dispatcherServlet一个modelandview
- dispatcherServlet将modelandview发送给viewResolver
- viewResolver返回dispatcherServlet 页面对象和model对象
- dispatcherServlet将返回的数据交给view进行渲染,再返回给dispatcherServlet
- dispatcherServlet将结果返回给客户端
Spring用到了哪些设计模式?
- 工厂模式: beanFactory、FactoryBean
- 适配器模式:AdvisorAdapter、HandlerAdapter
- 访问者模式:PropertyAccessor接口
- 装饰者模式:BeanWrapper
- 代理模式:aop
- 观察者模式:spring事件监听机制
- 策略模式:excludeFilters
- 模版模式:jdbcTemplate
- 委派模式:
- 责任链模式:beanPostProcesser,aop的方法调用
- 原型模式:
- 单例模式:Bean的实例
Spring 如何处理循环依赖问题?
-
循环依赖出现的情况:
1)自己依赖自己
2)两个bean互相依赖
3)A依赖B,B依赖C,C依赖A
-
Spring设计了三层依赖去解决循环依赖问题(DefaultSingletonBeanRegistry 中getSingleton方法,三级缓存依次为:singletonObjects(单例池), earlySingletonObjects,singletonFactories);当我们通过getBean()方法去获取一个对象实例的时候;spring会去一级缓存找,如果没有找到会去二级缓存找,如果都没找到,说明这个对象还没有实例化;于是Spring 容器会去初始化这个bean,而初始化实例的bean是早期bean,会放到二级缓存。三级缓存用来存储代理bean,当调用getbean()的时候发现需要通过代理工厂来创建,这个时候会把创建好的代理bean放到三级缓存,最终会把创建好的bean存入一级缓存;
-
无法解决循环依赖的情况
SpringMVC中的控制器是不是单例模式?如果是,如何保证线程安全?
控制器是单例模式,因此单例模式下就存在线程安全问题(不同线程操作同一个对象,会有问题);
spring中如何保证线程安全的方法:
-
将scope修改成prototype,request
-
最好的方式是将控制器设计成无状态模式。在控制其中,不要携带数据,但是可以引用无状态的service和dao
无状态:该对象中不存在属性
@Autowired 和 @Resource 区别
- @Autowired 依赖注入的时候是先byType 再byName;@Resource与之相反
- @Autowired是spring层面的注解,@Resource是java层面的注解
Synchronized底层实现?
synchronize修饰代码块的时候,底层是由一对monitorenter/monitorexit指令来实现的,monitor对象是同步的基本实现单元;
synchronized修饰方法的时候,会生成ACC_SYNCHRONIZED标识,表明该方法是一个同步方法
Synchronized锁升级原理
在锁对象的对象头(markword)中会存储锁的信息,里面有一个threadid字段,在第一次访问的时候threadid为空,jvm让其持有偏向锁,并将threadid设置为其线程id,再次进入的时候会先判断threadid是否与其线程id一致,如果一直则可以直接使用此对象,如果不一致则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数后,如果还没有正常获取到锁,则锁会从轻量级升级为重量级锁,此过程就构成了synchronized的升级;
锁升级的目的:是为了减低锁带来的性能消耗。在jdk1.6之后优化synchronized的实现方式,使用了偏向锁升级为轻量级锁升级为重量级锁的方式。
Synchronized 和 ReentrantLock:
- 两者都是独占锁,只允许线程互斥的访问临界区:synchronized加解锁的过程是隐式的,用户不用手动操作;ReentrantLock需要手动加锁解锁,并且解锁操作要放在finally代码块中,保证线程正常释放锁;使用灵活
- 两者都是可重入锁:ReentrantLock在重入时要确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁
- 都可以实现线程之间等待通知机制:synchronized结合Object上的wait()和notify()方法可以实现线程之间通信;ReentrantLock结合Condition接口同样可以实现线程之间通信
- ReentrantLock是java层面的实现,synchronized是JVM层面的实现
- synchronize会自动释放锁,而ReentrantLock需要手动释放锁
- synchronized是非公平锁,而ReentrantLock默认是非公平锁也可以实现公平锁
- ReentrantLock可以设置超时获取锁
- ReentrantLock上等待获取锁的线程是可中断的,线程可以放弃锁;而synchronized会无限期等待下去
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁
ThreadLocal 是什么?有哪些使用场景
ThreadLocal 为每个使用该变量的线程提供一个变量副本;所以每一个线程可以独立的改变自己的副本的值,而不影响其他线程所对应的副本;本质是ThreadLocal 内部维护了一个ThreadLocalMap,key是ThreadLocal本身,值是变量副本。key是弱引用,即存在引用关系的情况下可以被gc回收掉,导致我们的value 无法可达,造成内存泄漏。
使用场景:数据库连接和session管理
votaile 和 Synchronized的区别?
votaile 是用来修饰变量的,Synchronized可以用来修饰类,方法,代码块
volatile 仅可以保证变量修改的可见性,不能保证原子性;Synchronized 可以保证变量修改的可见性和原子性
volatile不会造成线程的阻塞;synchorinzed可能会造成线程的阻塞
volatile关键字原理
volatile是java并发编程中的一个关键字,它能够保证数据操作的可见性和有序性;
volatile禁止指令重排序来保证有序性
volatile对变量进行写操作时,jvm会向处理器发送一条lock前缀的指令,将这个缓存中变量回写到主内存中,所以如果一个变量被volatile所修饰的话,在每次数据变化之后,值都会被强制写入主内存,而其他处理器的缓存由于遵守缓存一致性协议,就会把变量的值从主存读取到自己的工作内存中。
线程池相关
关键参数:
corePoolSize:核心线程数;当任务数小于核心线程数时,有新的任务进来时,会创建新的线程来执行
workQueue:任务队列;当任务数大于核心线程数时,新的任务会存在任务队列中;当任务队列满了,会创建新的线程;线程数小于等于最大线程数
maximumPoolSize:最大线程数
keepAliveTime:空闲线程存活时间;超出corePoolSize后创建的线程存活时间
timeUnit:单位
threadFactory:线程工厂
handler:拒绝策略;AbortPolicy,DiscardPolicy,DiscardOldestPolicy,CallerRunsPolicy;RejectedExecutionHandler
线程池状态
RUNNING: 运行状态;线程池创建好之后就会进入此状态,如果不手动关闭,则则整个运行期间都是该状态
SHUTDOWN:关闭状态;不再接受新任务的提交,但是会将已保存在任务队列中的任务处理完
STOP:停止状态;不再接受新任务的提交,并且会中断当前正在执行的任务,放弃任务队列中已有的任务
TIDYING:整理状态;所有的任务执行完毕之后,当前线程池中的活动线程数将为0时的状态。此状态之后,会调用线程池的terminated()方法
TERMINATED:销毁状态;当执行完线程池的terminated()方法之后就会变成此状态
线程池监控
定时获取线程池的运行数据
taskCount: 线程池需要执行的任务数量
completedTaskCount: 线程池在运行过程中已完成的任务数量,小于或者等于taskCount
largestPoolSize: 线程池里曾经创建过的最大线程数量,通过这个数据可以知道线程池是否曾经满过
getPoolSize: 线程池的线程数量
getActiveCount: 获取活动的线程数
线程状态
NEW: 初始化状态
RUNNABLE:可运行状态
BLOCKED:阻塞状态
WAITING:无时限等待状态
TIMED_WAITING:有事先等待状态
TERMINATED:终止状态
ReentrantLock可重入原理
默认是非公平锁
首先通过CAS来获取锁,判断当前锁对象的state是否为0,如果为0,则获取锁成功,否则获取锁失败
其次,通过Aqs的tryAcquire()方法来获取锁,首先通过cas尝试获取锁,如果没有获取到锁则判断当前线程是否为持有锁的线程,如果是则获取到锁,同时state+1,如果未获取到锁,则将当前线程包装成一个node节点,加入到队列中;如果当前节点的 则将当前节点设置为头节点;不然调用shouldParkAfterFailedAcquire()方法,将上一个节点的waitstatus值改为-1
Redis
RDB方式bgsave的基本流程:
fork主进程得到一个子进程,共享内存空间
子进程读取内存数据写入新的rdb文件
用新的rdb文件替换旧的rdb文件
RDB的缺点:
RDB执行的间隔较长,两次rdb之间写入有丢失数据的可能;sava 30 1
RDB fork主线程,压缩,写出RDB文件比较耗时
Aof 每次执行写命令,会将命令存入aof buffer中 然后再进行持久化
默认情况下,aof也可能会丢失1s的数据
redis 过期删除策略:
- 定时删除
- 惰性删除
- 定期删除
redis 的过期策略包含定期删除和惰性删除。定期删除策略就是redis内部会起一个定时任务,会定期删除一些过期的key;惰性删除是指用户查询某个key时,会先判断该key是否过期,过期则删除;
但是两种策略都不能保证将过期key一定删除,漏网之鱼会越来越多,还可能导致内存溢出,当内存不足时,redis会做内存回收,内存回收采用LRU策略,即最近最少使用。
Spring是如何整合Mybatis将Mapper接口注册为Bean的原理
- 首先Mybatis的Mapper接口核心是JDK动态代理
- Spring会排除接口,无法注册到ioc容器中
- Mybatis实现了BeanDefinitionRegistryPostProcessor可以动态注册BeanDefinition
- 需要自定义扫描器(继承Spring内部扫描器ClassPathBeanDefinitionScanner)重写排除接口的方法(isCandidateComponent)
- 虽然接口注册成了BeanDefinition但是无法实例化Bean,因为接口无法实例化
- 需要将BeanDefinition的BeanClass替换成JDK动态代理的实例
- Mybatis通过FactoryBean的工厂方法设计模式可以自由控制Bean的实例化过程,可以在getObject方法中创建jdk动态代理
ThreadPool 和 ForkjoinPool 区别?
Openfeign的底层?
常见rpc框架?
dubbo, grpc,Thrift
@Configuration 和@Component区别
添加了@Configuration 注解之后,会对当前这个配置类使用cglib动态代理创建一个代理对象,去执行普通方法的时候,也是会用代理对象来执行方法
负载均衡策略?
随机策略、轮询策略、权重策略(如果一个服务的平均响应时间越短则权重越大,那么该服务实例被选中执行任务的概率也就越大)、BestAvailableRule(过滤掉失效的服务实例的功能,然后顺便找出并发请求最小的服务实例来使用)、ZoneAvoidanceRule(默认规则,复合判断服务所在区域的性能和server的可用性选择服务器)
CDN
分布式和微服务的区别?
分布式和微服务是两个完全不同的概念。微服务是系统架构的一种设计方式,它是将复杂的业务拆分成多个微小的服务,然后这些服务可以单独的运行和部署。服务与服务之间是通过rpc调用。分布式是系统部署的一种方式,将一个服务拆分部署到多台机器上,这样可以分担单台机器的负载压力,因此会存在一些分布式问题
http和rpc区别?
HTTP是一种通信协议,RPC是一类通信协议的统称,如rmi协议,dubbo协议和thrift协议都属于rpc协议;http也是rpc的一种。RPC翻译过来叫做远程过程调用,主要解决的是服务之间调用的体验,使得调用远程服务像调用本地方法一样简单;而http协议又叫超文本传输协议侧重解决某个特定场景下一个通信问题;就像邮件传输协议我们会用SMTP协议,文件传输协议我们会用FTP协议,网络直播会用RMTP协议一样;这些协议底层一般都是基于TCP协议来完成的。
跨域的原因及解决办法
由于浏览器的同源策略的限制,它要求web应用程序只能访问与当前页面具有相同协议、主机名和端口号的资源。
- @CrossOrigin 注解
- 实现WebMvcConfigurer 接口中addCorsMappings()方法
- 使用自定义Filter来控制访问
spring 事件监听的核心机制是什么
spring事件监听的核心机制是采用了观察者模式;
时间监听由三部分组成:事件(负责对应相应监听器)、监听器(观察者)、时间发布器(负责通知观察者)
java 保证线程安全的方式有哪些
- 针对原子性:
- JDK提供了Atomic类,通过CAS来保障原子性
- java还提供各种锁机制,比如synchronized关键字
- 针对可见性
- synchronized, voltile
springcloud
AQS的理解
AQS是线程同步器,是juc包中多个组件的底层实现,比如lock,countdownLatch和semaphore都用到了AQS。从本质上来说,aqs提供了两种锁的实现机制,分别是排他锁和共享锁。Reentranlock重入锁是排它锁的一种实现,同一时间只允许一个线程去操作共享资源。countdownLatch 和semphore是共享锁的实现。
AQS采用一个int类型的互斥变量state,用来记录锁竞争的一个状态,0表示当前没有任何线程竞争锁资源,而大于等于1表示已经有线程持有锁资源;aqs采用cas机制去保证state互斥变量更新的原子性;未获取到锁的线程通过Unsafe类中的park方法去进行阻塞,把阻塞的线程按照先进先出的原则加入到一个双向链表的结构中,当获得锁资源的线程释放锁后,会从这样一个双向链表的头部去唤醒下一个等待的线程,再去竞争锁。
关于锁公平性和非公平性的问题,AQS在竞争锁资源的时候,公平锁需要去判断双向链表中是否有阻塞的线程,如果有则需要排队去等待;而非公平锁的处理方式是,不管双向链表中是否存在等待竞争锁的线程,他都会直接去尝试更改互斥变量state去竞争锁
线程池如何知道一个线程的任务已经执行完成
不管线程池内部还是外部,想要知道线程是否执行结束,我们必须获取线程执行结束后的状态,而线程本身是没有返回值的,所以我们只能通过阻塞-唤醒的方式来实现
一、从线程池内部:线程池会调度工作线程去执行线程的run()方法,当run()方法执行完成以后说明这个任务也完成了
二、线程池外部:submit()方法提供一个future的返回值,feture.get()方法有返回则说明任务执行结束;引入countdownLatch计数器,它可以通过初始化指定的一个计数器,它提供两个方法一个是await(),countdown();当计数器为0时,唤醒处于await()方法阻塞的线程。
阻塞队列的有界和无界
阻塞队列是一种特殊的对列,它在普通队列的基础上提供了两种附加的功能;一是当对列为空时,获取对列中元素的消费者线程会被阻塞同时会唤醒生产者线程;当对列中元素满了时,向队列中添加元素的生产者线程会被阻塞同时会唤醒消费者线程;其中阻塞队列中能够容纳的元素个数通常情况下是有限的;比如ArrayBlockingQueue,可以在构造方法中传入队列个数,而无界队列则是没有设置固定大小的队列,如LinkedBlockingQueue 它的元素存储默认为Integer.MAX_VALUE;无界队列存在很大风险,在并发量很大的情况下,线程池中几乎可以无限制的添加任务,容易导致内存溢出的问题。
ConcurrentHashMap的底层实现
JDK1.7 | JDK1.8 | |
---|---|---|
存储 | 数组+链表 | 数组+链表+红黑树 |
位置算法 | h&(length-1) | h&(length-1) |
链表超过8 | 链表 | 红黑树(数组长度大于64) |
节点结构 | Entry<K,V> | Node<K,V> |
插法 | 头插法 | 尾插法(解决链表环问题) |
ConcurrentHashMap是有数组,单向链表以及红黑树来构成的;当我们初始化一个concurrentHashMap时会默认初始化一个长度为16的数组
ConcurrentHashMap在hashMap的基础上提供了一个并发安全的实现。主要是通过对于node节点去加锁,来保证数据更新的安全性。
在jdk1.8中,ConcurrentHashMap锁的粒度是数组中的一个节点,而在jdk1.7中,它锁定的是segment,锁的范围更大,所以他的性能会更低;引入红黑树,降低了数据查询的时间复杂度。多线程并发扩容,多个线程对数组进行分片,分片之后,每个线程去负责一个分片的数据迁移,从而提高了分片过程中数据迁移的一个效率。
CAS机制
CAS:CompareAndSwap,是Unsafe类里面的一个方法,主要功能是保证在多线程环境下,对共享变量修改的一个原子性。 是乐观锁的一种实现
ABA问题
while 循环问题
发生死锁的条件
- 互斥条件,共享资源X和Y只能被一个线程占用
- 请求和保持条件,线程T1已经取得共享资源X,在等待共享资源Y的时候,不释放共享资源X
- 不可抢占条件,其他线程不能强行抢占线程T1占有的资源
- 循环等待条件,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源就是循环等待
wait()和notify() 为什么要放到synchronized代码块中
wait和notify是用来线程间的通信,wait让线程进入到阻塞状态,notify让阻塞的线程被唤醒;在多线程中要实现通信可以通过共享变量的方法来实现。
一个线程需要调用对象的wait方法时,这个线程必须拥有这个对象的锁,接着它就会释放这个对象锁并进入等待状态知道直到其他线程调用这个对象上的notify()方法;同样的,当一个线程需要调用对象的notify()方法的时候,他会释放这个对象的锁,以便其他在等待的线程可以得到这个对象锁。由于两个方法都需要线程持有对象的锁,所以可以通过synchronized关键字来实现
ReentrantLock的理解
ReentrantLock为可重入锁,公平和非公平锁
可重入锁:当线程持有锁对象时,再次请求锁的时候,无需等待即可获取到锁资源;
公平和非公平锁在于当线程获取锁失败时,会调用unsafe.park()方法来阻塞线程,并将线程按照先进先出的顺序放入一个双向链表的结构中,当持有锁的线程释放锁资源后,公平锁会从双向链表的头部唤醒线程,而非公平锁则让队列中的线程去竞争锁,去获取锁资源
ReentrantLock基于AQS,引入了一个int类型的互斥变量来记录锁竞争的状态。
对于线程池的理解
池化技术-资源复用思想
- 减少线程的频繁创建和销毁带来的性能开销,因为线程创建会涉及到cpu上下文切换,内存分配等工作
- 线程池本身会有些参数来控制线程创建的数量,这样就可以避免无休止的创建线程带来的资源利用率过高的问题,起到了资源保护的作用
- 线程的参数解释:核心线程数,最大线程数,阻塞队列,拒绝策略,线程工厂
线程池中阻塞队列的作用?为什么是先添加阻塞队列而不是先创建最大线程
- 一般的队列只能存储有限长度的数据,当超过了队列长度就无法保存当前的任务了,阻塞队列可以通过阻塞保留住当前想要入队的任务;没有任务时可以阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源;自带唤醒阻塞的功能,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活,不至于一直占用cpu资源
如何终止一个线程
线程是一个系统级的概念,线程最终的执行和调度是由操作系统来决定的,在java中我们通过Thread.start()方法来启动线程,不一定立刻执行;理论上我们去中断一个线程只能像linux中kill命令一样去结束进程。所以java thread中提供了一个stop方法去强制终止,但是这种方式是不安全的,因为有可能这个任务的线程还没有执行完成,导致出现运行结果不正确的现象;要想安全的去终端一个线程,thread中提供了一个interrupt()的方法,需要配合isinterrupt()方法去使用就可以实现线程的安全中断
CompleteFuture的理解
CompleteFuture是java8引入的一个组件,它能够处理异步任务和异步任务的结果;在它出现之前我们只能使用callable/future这样一个机制来获取异步线程的执行结果,future是通过阻塞等待发方式来实现的,对性能不是很友好;而CompleteFuture可以让我们将一个耗时的任务交给线程池来异步处理,然后可以继续执行其他任务;等到异步任务执行结束后,会触发一个回调方法。我们可以在回调方法里去处理异步任务的执行结果
Spring 有哪几种方式可以将bean注入到容器中
- 通过xml的方式配置
- 通过@ComponentScan 扫描包路径下的@Controller @Service @Component 等
- 通过@Configuration 注解下@Bean方式
- 通过@Import方式导入
- 通过实现FactoryBean接口,来注入某一种类型的bean
- 通过实现ImportSelector接口来注入
ThreadLocal 会发生内存泄露吗
Threadlocal 为每个使用共享变量的线程提供一个变量副本,使得每个线程操作变量副本不会影响到其他线程的变量副本,从而保证线程安全;Threadlocal 中为维护了一个threadLocalMap,key为线程本身,值为变量副本;然而key为弱引用,从而导致即使存在引用关系,key也能被gc回收,导致key对应的value没有引用,从而导致内存泄露。
解决办法:
- 每次使用完ThreadLocal之后,可以调用ThreadLocal的remove()方法来清除数据
- 将ThreadLocal变量定义为private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到entry的value值,进而清除掉
强引用、软引用、弱引用、虚引用
不同的引用主要体现的是对象不同的可达性状态和对于垃圾收集的影响
强引用就是普通对象的引用,只要还有强引用指向一个对象就表示对象还活着,垃圾收集器无法回收这一类的对象
软引用是一种相对于强引用若一些的引用,可以让对象豁免一些垃圾收集,只有当jvm认为内存不足的时候,才会去试图回收引用指向的对象
弱引用是相对于强引用而言的,它是允许在引用关联的情况下,被垃圾回收的一个对象
mybatis 缓存机制
mybatis设计了二级缓存来提升数据的检索效率,避免每一次的数据检索都去查询数据库;一级缓存是 sqlSession级别的缓存,也叫本地缓存localCache,每个用户在执行查询的时候,都需要使用sqlsession来执行,为了避免每一次都去查询数据库mybatis把查询出来的数据缓存到sqlsession的本地缓存里面,后续的sql如果在命中缓存的情况下,就可以从缓存中读取这样一个数据;二级缓存是sqlsessionFactory这样一个级别的缓存,当多个用户去查询数据的时候,只要有任何一个sqlsession拿到了数据,都会放到二级缓存里面,其他的sqlsession就可以从二级缓存里面去加载数据。
一级缓存是默认开启的,如果需要关闭需要,需要到mybatis-config文件中,将localCacheScope 这个属性值设置为Statement,默认值为Session
二级缓存开启需要将mybatis-config配置文件中的cacheEnabled 属性设置为true,在对应的映射文件中添加一个cache标签
为什么先找二级缓存,再找一级缓存
二级缓存是sqlSessionFactory 级别的缓存,作用域大,缓存数据大
一级缓存是sqlSession级别的一个缓存,作用域小,缓存数据少
先查二级缓存再查一级缓存能提高我们的查询效率
mybatis是如何实现分页
mybatis实现分页有两种方式:逻辑分页和物理分页
针对物理分页:mybatis 可以利用mysql limit的分页关键词进行分页;或者基于mybatis里面的interceptor,在执行sql之前,拦截sql动态拼接分页关键词
针对内存分页:使用mybatis 提供的RowBounds对象,来进行内存级别的分页
MyBatis工作原理
mybatis配置文件包括全局配置文件和mybatis映射文件,其中配置文件包含了数据源以及事务信息;映射文件配置了sql执行的相关信息。mybatis通过读取配置文件信息构造出SqlSessionFactory,即会话工厂
通过sqlSessionFactory,可以创建出sqlSession会话。Mybatis是通过sqlSession来操作数据库的,它本身是不能直接操作数据库,是通过底层的Executor执行器接口来操作数据库。Executor接口有两个实现类,一个是普通执行器,一个缓存执行器。Executor执行器将要处理的sql信息封装到一个底层对象mappedStatement中;通过statementHandler,paramterHandler,ResultsetHandler
ACID靠什么保证
A:原子性由undo.log日志来保证以及三个隐藏字段,他记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql
C:一致性由其他三大特性来保证,程序代码要保证业务上的一致性
I:隔离性由MVCC来保证
D:持久性由内存+redo.log来保障,宕机时可以从redo log恢复
分布式事务解决方案
强一致性:XA协议:2阶段提交;prepare和commit TM 和RM
最终一致性:TCC try comfirm cancel
消息队列丢失
在消息生产端,给每个发出的消息都指定一个全局唯一id,或者附加一个连续递增的版本号,然后在消费端做对应的版本校验;利用拦截器在发送消息之前将消息版本号注入消息中,再通过拦截器检测版本号的连续性或消费状态
Rocketmq 如何解决重复消费问题
我们可以利用幂等性来处理
方案1. 利用数据库的特性,建立一张去重表,表中存在一个unique字段来表示是否消费过
方案2. 利用乐观锁机制,消息发送的时候多携带一个版本号的字段
RocketMQ 如何保证高可用
首先我们可以采用多master多slave的方案,master和slave之间采用同步复制,异步刷盘策略;同步复制,只有当master节点将数据同步到slave节点时,才会返回ack标识;说明此时数据已经保存到slave内存中;采用异步刷盘策略可以保证mq性能;
Rocketmq 的存储机制
rockermq的消息存储在commit log中,broker默认会给commitlog申请1G的磁盘空间,这是为了保证存储空间是有序的。为什么要申请连续的存储空间?因为rocketmq中使用了零拷贝技术来读取文件进行刷盘,减少用户空间和内核空间的切换,可以极大地提升读取文件的性能。
rocketmq默认给每个topic创建4个队列,每个队列有一个queuelog;
consumequeue可以看做是commitlog的索引文件,查找的时候就想数组那样根据索引下标查找元素,然后更具commitLogOffset和msgSize从commitlog中获取消息内容
rocketmq 消费方式
Rocketmq是基于发布订阅模型的消息中间件,也就是说consumer订阅了broker上的某个topic,当producer发布消息到topic上时,consumer就能收到该消息
pull模式:consumer主动去broker中拉取消息,取消息的逻辑需要用户自己来写,而且需要管理offset,比较复杂
push模式:此模式下broker收到消息后会主动推送给consummer,对于push consumer,由用户注册MessageListener来消费消息,push模式在底层是通过pull实现的,consumer开启向broker长轮询来批量拉取数据,会不停的询问broker有没有消息,有消息的话会进行拉取消费
**Redis IO多路复用底层 **
redis io多路复用模型是有效解决单线程的服务端使用不阻塞的方式来处理多个client端请求问题。
因为redis是单线程的,所有的操作都是按照顺序线性执行的,但是由于读写操作需要等待用户输入或输出都是阻塞的,所以io操作在一般情况下往往不能直接返回,这导致了某一个客户端的io阻塞,导致整个进程无法对其他客户端提供服务,而io就是为了让单线程的服务端应用同时处理多个客户端的请求,Redis采用了io多路复用机制。
多路指的是多个网络连接服务端
复用指的是复用同一个线程,即使用一个线程来检查跟踪多个socket的状态来处理多个io流
一个socket客户端与服务端连接时,会生成对应一个套接字描述符(fd文件描述符的一种),每一个socket网络连接都对应一个文件描述符
多个客户端与服务端连接时,redis采用多路复用机制将客户端socket对应的fd注册到一个监听列表中,当客户端执行read,write等操作命令时,io多路复用程序会将命令封装成一个事件绑定到对应的fd上
文件事件处理器使用io多路复用模块同时监控多个文件描述符的读写情况,当accept、read、write和close文件事件产生时,文件事件处理器就回调fd绑定的事件处理器进行相关命令操作
当多个客户端连接redis服务端时,服务端会将每个客户端socket对应的fd注册到epoll,然后epoll同时监听对多个fd的读写操作,当其中一个client端达到读写的状态,文件事件处理器就马上执行,从而不会出现io阻塞的问题,提高了网络通信的性能。
io多路复用模型利用select,poll,epoll函数可以同时监听多个流的io能力,在空闲的时候会把当前线程阻塞,当一个或多个流有事件的时候,会把线程从阻塞中唤醒,于是程序会轮询一遍所有的流(epoll只轮询那些真正发出了事件的流),依次顺序的处理就绪的流,这种做法就避免了大量无用的等待操作
Redis 主从集群数据同步原理
主从第一次同步时全量同步:
第一阶段:1.0 执行relicaof 命令建立连接 1.1 请求同步数据 1.2 判断是否是第一次同步 1.3 是第一次返回master的数据版本信息 1.4 保存版本信息
第二阶段:执行bgsave 生成rdb文件;发送rdb文件; 清空本地数据,加载rdb文件;生成rdb期间会将所有的命令记录到repl_bak log中
第三阶段:发送repl_baklog中的命令 ;执行收到的命令
简述全量同步和增量同步区别
- 全量同步,master将完整内存数据生成rdb,发送rdb到slave。后续命令记录在repl-baklog中,逐个发送给slave
- 增量同步:slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave
什么是执行全量同步
- slave节点第一次链接master节点时
- slave节点断开时间太久,repl_baklog中的offset已经被覆盖时
什么时候执行增量同步
- slave节点断开又恢复,并且在repl_baklog中能找到offset时
Redis 哨兵机制
redis哨兵机制来实现主从集群的自动故障恢复。
监控:sentinel会不断检查master和slave节点是否正常工作
自动故障恢复:如果是master故障,sentinel会将一个slave提升为master,当故障实力恢复后也以新的master为主
通知:sentinel充当redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给redis的客户端
sentinel基于心跳机制检测服务状态,每隔1s向集群的每个实例发送ping命令:
主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线
客观下线:若查过指定数量的sentinel都认为该实例主观下线,则该实例客观下线;指定数量最好超过sentinel实例数量的一半
选举新的master依据
首先会判断slave节点与master节点断开时间长短,如果超过指定值,则会排除该slave节点
然后判断slave节点的slave-priority值,越小优先级越高,如果是0则不参与选举
如果slave-priority值一样,则判断slave节点的offset值,越大说明数据约新,优先级越高
最后判断slave节点的运行id大小,越小优先级越高
如何实现故障转移
sentinel给备选的slave1节点发送salve of no one命令,让该节点成为maser节点
sentinel给其他节点发送salveof master节点的ip+ 端口号命令,让这些节点成为master的slave节点,开始从新的master节点上同步数据
最后将故障的master节点标记为slave节点,故障恢复后成为新节点的slave节点
Redis 大key解决方案
bigkey 定位:1)通过redis客户端自带的redis-cli --bigkeys 扫描,显示信息不全 2)使用三方工具redis-rdb-tools ,这个过程会先使用bgsave命令fork主线程来dump一个rdb文件,然后对这个文件进行分析 3)通过java程序 利用scan命令来循环查找bigkey
bigkey 解决:1)针对bigkey进行拆分 2)清理无效数据 3)对象压缩
Integer面试题
Integer是一种包装类,是对基本类型int的包装;所以integer除了int类型的基本操作之外,他还引入了享元模式的设计,Integer会把-128 到127之间的数据加一层缓存,也就是当Integer存储的值在-128到127之间的时候,会直接从缓存中去获取;如果不在这个区域内,会直接创建一个新的对象返回。
这样设计的初衷减少频繁创建对象带来的内存消耗来提升性能
MySQL
-
索引的分类
数据结构:B+树索引,hash索引,full_text索引
物理存储:聚簇索引(主键索引),二级索引(辅助索引)
字段特性:主键索引,唯一索引,普通索引,前缀索引
字段个数:单列索引,联合索引
-
Innodb的索引结构
innodb采用b+树的索引结构来存储数据,他是一种聚簇索引,即叶子节点存放主键索引和物理文件,其他节点存放索引,叶子节点之间采用双向链表的结构进行连接。再加上mysql读取数据是通过页的方式来读取了,数据存储在叶子节点能够一页上读取更多的数据,减少和磁盘的io次数,提升了效率;同时平衡二叉树又是一种平衡树,这使得根节点到叶子节点的路程都差不多,使得mysql的查询更加稳定。
-
索引下推
索引下推使用于二级索引也就是辅助索引减少全行记录读取,减少回表次数,从而减少io操作;
在普通的查询中,数据库需要先从表中读取所有的数据记录,然后再根据查询条件过滤不需要的记录,最后返回查询结果。而在索引下推中,数据库会在索引树的节点上进行条件过滤,只将满足条件的数据返回,而不是读取整个数据记录。这样可以避免从磁盘读取不必要的数据,降低io开销,提升查询速度。
-
为什么数据库字段建议设计成not null
提高查询性能,避免额外去处理空值的情况
数据一致性,在开发层面可以去避免错误处理空值情况
gc的年龄为什么要设置为15次
我们的对象头markword中会存储对象信息,其中gc信息分配了4bit位来存储,4bit最大值为15
应用突然出现OOM异常,如何排查
对于还在正常运行的系统:
- 可以使用jmap来查看jvm中各个区域的使用情况 jps + jmap -heap 进程号
- 可以通过jstack来查看线程的运行情况,比如哪些线程阻塞,是否出现了死锁
- 可以通过jstat来查看垃圾回收情况,特别是fullgc
- 通过各个命令的结果或者jvisual vm等工具来分析
对于已经发生了oom的系统:
- 生产系统中都会设置当系统发生了OOM时,生成当时的dump文件(-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)
- 利用jvisual vm等工具来分析dump文件
- 根据dump文件找到异常的实例对象和异常的线程 ,定位到具体的代码
transient关键字的作用是什么?什么场景下会用到
transient用来修饰变量,被transiten修饰的变量不会参与序列化过程。即被该关键字修饰的属性在反序列化的时候会被解析为null或者其他默认值
序列化用户信息时,密码属性用该关键字修饰
@RestController的工作原理
@RestController是一个复合注解,包含了@Controller和@ResponseBody
@Controller注解表示这是一个控制器,用于处理http请求和响应,而@ResponseBody注解表示方法返回的结果可以直接作为Http相应的内容即JSON或XML格式的数据,而不是返回一个视图
Tomcat 为什么要使用自定义类加载器
一个tomcat容器中可以部署多个应用,而每个应用中都存在很多类,并且各个应用的类都是独立的,全类名是可以相同的,而tomcat启动之后,就是一个java进程,如果只存在一个类加载器,则会有问题;所以会为部署的每个应用都生成一个类加载实例。名字叫做webAppClassLoader,这样tomcat中每个应用就可以使用自己的类加载器去加载自己的类
什么是Innodb中的页?有什么作用?
页的大小:16kb
如何控制bean的创建顺序
针对于两个bean可以使用@Dependson注解
针对多个bean,我们可以在扫描类得到beanDefiniton对象之后,自定义容器初始化器自己实现ApplicationContextInitializer,同时该类需要定义在Spring.factories文件中;同时需要自己定义一个类实现BeanDefintionRegistryPostProcessor,往list中存入首先创建对象的BeanDefinition对象
线程池中的任务执行原理