Java面试题第二篇
- 1. 并发的三大特性
- 2、线程池、解释线程池参数
- 3、BeanFactory和ApplicationContext有什么区别?
- 4、描述一下Spring Bean的生命周期
- 5、Spring的几种Bean的作用域
- 6、单例Bean是线程安全的吗?
- 7、Spring框架用到了哪些设计模式
- 8、Spring事务的实现方式、隔离级别、传播行为
- 9、Spring事务什么时候会失效
- 10、 依赖注入与bean装配
- 11、SpringBoot、SpringMVC、Spring有什么区别
- 12、SpringMVC工作流程
- 13、Java SPI
- 14、SpringBoot自动配置原理
- 15、SpringBoot的starter
1. 并发的三大特性
- 原子性:在一个操作中CPU不可以在中途暂停然后再调度,即不被中断操作(维护线程安全)
- 注意:程序原子性指的是最小的操作单元,比如自增操作,它本身其实并不是原子性操作,分了3步的,包括读取变量的原始值、进行加1操作、写入工作内存。所以在多线程中,有可能一个线程还没自增完,可能才执行到第二步吗,另一个线程就已经读取了值,导致结果错误。那如果我们能保证自增操作是一个原子性的操作,那么就能保证其他线程读取到的一定是自增后的数据
/*
1. 将count从主存读到寄存器
2. +1的运算
3. 将结果写入寄存器
4. 将寄存器的值刷回主存(什么时候刷入由操作系统决定,不确定的)
*/
private long count = 0;
public void calc(){
count++;
}
- 可见性:当多个线程访问一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。若两个线程在不同的CPU,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程2没看到,这就是可见性问题。
- 有序性:虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码虚拟机不一定会按照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之后,虽然对变量的值没有造成影响,但有可能会出现线程安全问题
- 关键字:volatile本身就包含了禁止执行重排序的语义,synchronized就是同步锁
int a = 0;
boolean flag = false;
public void write(){
a = 2;
flag = true;
}
public void multiply(){
if(flag){
int ret = a*a;
}
}
/*
write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,
ret直接计算出结果,再到线程1,这时候a才赋值为2,很明显迟了一步。
*/
2、线程池、解释线程池参数
- 为什么使用线程池
- 降低资源消耗:提高线程利用率,降低创建和销毁线程的消耗。(因为线程的创建和消耗都是比较耗费资源的,所以使用线程池,将创建的线程保存在线程池中,需要使用的时候拿出来,不需要反复创建和销毁)
- 提高响应速度:任务来了,直接从线程池中获取,而不是创建线程再执行
- 提高线程可管理性:线程是稀缺资源(个数有限),使用线程池也可以统一分配调优监控
- 线程池的参数
- corePoolSize:代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,而是一种常驻线程。
- workQueue:用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进行则全部放入队列,直到整个队列被放满但任务还再持续进入则开始创建新的线程。
- maxinumPoolSize:代表最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用完了,将workQueue都占满了,还无法满足需求时,此时就会创建新的线程,但是线程池内总数不会超过最大线程总数。
- keepAliveTime、unit:表示超过核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超过核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过setKeepAliveTime来设置空闲时间。
- Handler:任务拒绝策略,有两种情况,第一种是当我们调用shutdown等方法关闭线程池后,这时候即使线程池内部还没执行完正在执行,但是由于线程池已经关闭,我们再继续想线程池提交任务就会遭到拒绝。第二种情况就是当达到最大线程数,线程池已经没有能力继续处理提交的任务时,这时候也会拒绝
- ThreadFactory:线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择自定义线程工厂,一般我们会根据业务来制定不同的线程工厂
举个栗子:设corePoolSize=5,workQueue.size=5,maxinumPoolSize = 10
- 现在来第1、2、3、4、5个任务,corePoolSize足够,所以可以直接创建线程。
- 再来第6、7、8、9、10个任务,corePoolSize满了,所以后面来的任务都得在workQueue排队
- 再来第11、12、13、14、15个任务,corePoolSize和workQueue都满了,所以需要创建Queue里面排队的线程(FIFO)
- 后面再来任务,workQueue.size和maxinumPoolSize都满了,所以使用Handler拒绝
注意:最大线程数 = maxinumPoolSize.size;最大任务数 = maxinumPoolSize.size+ workQueue.size
- 线程池的处理流程(按照上面例子看就行了)
- 线程池中的阻塞队列
- 注意:阻塞队列与workQueue并不是同一个队列
- 一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务
- 阻塞队列可以保证任务队列中没有任务时,阻塞常驻线程(corePoolSize创建的线程,它们会一直访问一个没有任务的workQueue,消耗CPU资源),使得线程进入wait状态,释放cpu资源
- 阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存储,不至于一直占用cpu资源
- 为什么是先添加队列而不是先创建最大线程?
- 在创建新线程的时候,是要获取全局锁的,这个时候其他的线程就得阻塞,影响了整体效率。
- 线程池中的线程复用原理
- 线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过Thread创建线程时的一个线程必须对应的一个任务的限制。所以我可以认为线程池的本质就是线程复用。
- 在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对于Thread进行了封装,并不是每次执行任务都会调用Thread.start()来创建新线程,而是让每个线程去执行一个循环任务,在这个循环任务中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的run方法,将run方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的run方法串联起来。
- 核心:start()方法是创建新线程来执行run()任务,线程池就是不通过start()方法创建新线程,而是用旧现成的run()方法来执行任务。
3、BeanFactory和ApplicationContext有什么区别?
- ApplicationContext是BeanFactory的子接口,所以ApplicationContext提供了更完整的功能
- 继承MessageSource,因此支持国际化
- 统一的资源文件访问的方式
- 提供在监听器中注册bean的事件
- 同时加载多个配置文件
- 载入多个上下文,使得每一个上下文都专注于一个特定的层次,比如应用的web层
ApplicationContext的使用实例
BeanFactory的使用实例
- 加载Bean的区别
- BeanFactory采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,我们就不能发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFactory加载后,直至第一次使用调用getBean方法才会抛出异常
- ApplicationContext是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,并且通过这样的预加载,可以确保当你需要的时候,就不用等待,因为它们已经创建好了。
- 基于上述的区别,ApplicationContext相对于BeanFactory的唯一不足是占用内存空间,并且启动较慢。
- 创建方式的区别
- BeanFactory通常以编程的方式被创建(命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。)
- ApplicationContext还能以声明的方式创建(·声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。)
- BeanPostProcessor、BeanFactoryPostProcessor的使用
ApplicationContext和BeanFactory都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但是两者之间的区别是ApplicationContext可以自动注册,BeanFactory需要手动注册
4、描述一下Spring Bean的生命周期
- 实例化普通对象
- 解析类得到BeanDefinition,即通过ComponentScan(“路径”)扫描指定路径,找到需要注册的类并解析类,然后就定义Bean
- 如果有多个构造方法,则要推断构造方法
- 确定好构造方法后,进行实例化得到一个普通对象
- 依赖注入
- 对对象中的加了@Autowired注解的属性进行依赖注入
- 回调Aware方法,比如BeanNameAware、BeanFactoryAware(参考文章)
- 初始化前中后方法
- 前:调用BeanPostProcessor的初始化前的方法(感觉是AOP)(参考文章)
- 中:调用初始化方法(执行所有方法)
- 后:调用BeanPostProcessor的初始化后的方法(感觉是AOP)
注意:BeanPostProcessor也就是Bean的后置处理,是在完成依赖注入后,即Bean实例化后
- 创建、使用、销毁
- 如果当前创建的bean是单例的则会把bean放入单例池
- 使用Bean
- Spring容器关闭时调用DisposableBean中的destroy()方法
5、Spring的几种Bean的作用域
- singleton:默认,每个容器中只有一个bean的实例
- prototype:为每一个bean请求提供一个实例
- request:bean被定义为在每个HTTP请求中创建一个单例对象,也就是说不同HTTP请求有不同单例对象,同一HTTP使用相同单例对象
- session:与request范围类似,确保每个session中有一个bean的实例,在session过期后,bean会随之失效
- application:bean被定义为在ServletContext的生命周期中复用一个单例对象
- websocket:bean被定义在websocket的生命周期中复用一个单例对象
6、单例Bean是线程安全的吗?
- Spring中的Bean默认是单例模式的,框架并没有对bean进行多线程的封装处理,即线程不安全
- 如果Bean是有状态的,那就需要开发人员自己来进行线程安全的保证,最简单的办法就是改变bean的作用域,把singleton改为prototype,这样每次请求Bean就相当于是new Bean(),这样就可以保证线程的安全了。
- 有状态就是有数据存储功能
- 无状态就是不会保存数据
- controller、service、dao层本身并不是线程安全的,如果只是调用里面的方法,而且多线程调用一个实例的方法,会在内存中复制变量,这是自己的线程的工作内存,是安全的。
- 不要在bean中声明任何有状态的实例变量或类变量,如果必须如此,那么就使用ThreadLocal把变量变为线程私有的。如果bean的实例变量或类变量需要在多个线程之间共享,那么就只能使用synchronized、lock等这些实现线程同步的方法了。
7、Spring框架用到了哪些设计模式
- 单例模式:在Spring中最明显的使用场景是在配置文件中配置注册bean对象的时候【设置scope的值为singleton】
<?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 class="com.dpb.pojo.User" id="user" scope="singleton">
<property name="name" value="波波烤鸭"></property>
</bean>
</beans>
- 原型模式:在Spring中最明显的使用场景是在配置文件中配置注册bean对象的时候【设置scope的值为prototype】
<?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 class="com.dpb.pojo.User" id="user" scope="prototype">
<property name="name" value="波波烤鸭"></property>
</bean>
</beans>
- 模板模式
- 核心是父类定义好流程,然后将流程中需要子类实现的方法就抽象化留给子类实现,Spring中JDBCTemplate就是这样的实现。例如,我们知道JDBC的步骤是固定:加载驱动、获取连接通道、构建sql语句、执行sql语句、关闭资源。
- 在这些步骤中第3步和第4步是不确定的,所以就留给客户实现,而我们实际使用JDBCTemplate的时候也确实是只需要构建SQL就可以了。这就是典型的模板模式。
- 观察者模式
- 观察者模式定义的是对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。使用比较场景是在监听器中,而Spring中Observer模式常用的地方也是listener的实现。如ApplicationListener.
- 简单工厂模式
- 简单工厂模式就是通过工厂根据传递进来的参数决定产生哪个对象。Spring中我们通过getBean方法获取对象的时候根据id或者name获取就是简单工厂模式了。
//ApplicationContext这里,一旦读取了bean.xml,那么Spring容器中的所有bean都被实例化了,即在bean.xml中的所有bean都被创建了,并且所有bean都执行了无参构造方法
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
//getBean : 参数即为spring配置文件中bean的id;
//就是将容器中“hello”这个JavaBean拿出来,注入到Hello hello中;
Hello hello = (Hello) context.getBean("hello");
hello.show();
- 工厂方法模式
- 在Spring中我们一般是将Bean的实例化直接交给容器去管理的,实现了使用和创建的分离,这这时候容器直接管理对象
- 还有一种情况是,bean的创建过程我们交给一个工厂去实现,而Spring容器管理这个工厂
- 在Spring中有两种实现一种是静态工厂方法模式,另一种是动态工厂方法模式
- 适配器模式
- 将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作,这就是适配器模式。
- 装饰者模式
- 装饰者模式又称为包装模式(Wrapper),作用是用来动态的为一个对象增加新的功能。装饰模式是一种用于代替继承的技术,无须通过继承增加子类就能扩展对象的新功能。使用对象的关联关系代替继承关系,更加灵活,同时避免类型体系的快速膨胀。 spring中用到的包装器模式在类名上有两种表现:一种是类名中含有Wrapper,另一种是类名中含有Decorator。基本上都是动态地给一个对象添加一些额外的职责。 具体的使用在Spring session框架中的
- SessionRepositoryRequestWrapper使用包装模式对原生的request的功能进行增强,可以将session中的数据和分布式数据库进行同步,这样即使当前tomcat崩溃,session中的数据也不会丢失。
- 代理模式
- 代理模式应该是大家非常熟悉的设计模式了,在Spring中AOP的实现中代理模式使用的很彻底.
- 策略模式
- 策略模式对应于解决某一个问题的一个算法族,允许用户从该算法族中任选一个算法解决某一问题,同时可以方便的更换算法或者增加新的算法。并且由客户端决定调用哪个算法,spring中在实例化对象的时候用到Strategy模式。
- 责任链默认
- AOP中的拦截器链
- 委托者模式
- DelegatingFilterProxy,整合Shiro,SpringSecurity的时候都有用到。
8、Spring事务的实现方式、隔离级别、传播行为
- Spring事务的实现方式
- spring的事务是对数据库的事务的封装,最后本质的实现还是在数据库,假如数据库不支持事务的话,spring的事务是没有作用的. .数据库的事务说简单就只有开启,回滚和关闭,spring对数据库事务的包装,原理就是拿一个数据连接,根据spring的事务配置,操作这个数据连接对数据库进行事务开启,回滚或关闭操作.但是spring除了实现这些,还配合spring的传播行为对事务进行了更广泛的管理.其实这里还有个重要的点,那就是事务中涉及的隔离级别,以及spring如何对数据库的隔离级别进行封装.事务与隔离级别放在一起理解会更好些.
- Spring事务的两种使用方式:编程式和申明式,@Transactional注解就是申明式的。
- 当然针对哪些异常回滚事务是可以配置的,可以利用@Transactional注解中的rollbackFor属性进行配置,默认情况下会对RuntimeException和Error回滚
- Spring事务的隔离级别
- Spring事务的传播行为
9、Spring事务什么时候会失效
- Spring事务的原理是AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用了,常见情况如下几种:
- 发生自调用,类里面使用this调用本类的方法,此时这个this对象不是代理类,而是UserService对象本身。(即放入Map<beanName,bean对象>的不是代理对象,而是普通对象)
-方法不是public的,因为@Transactional只能用于public的方法上,否则事务会失效。如果要用在非public方法上,可以开启AspectJ代理模式。(这是因为事务基于AOP,AOP基于继承,如果使用的是private,那么就无法继承这个方法,就会产生上述问题)- 数据库不支持事务
- 没有被Spring管理
- 异常被吃掉,导致事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)
10、 依赖注入与bean装配
依赖注入是什么?
当某个角色(可能是一个Java实例,调用者)需要另一个角色(另一个Java实例,被调用者)的协助时,在 传统的程序设计过程中,通常由调用者来创建被调用者的实例。但在Spring里,创建被调用者的工作不再由调用者来完成,因此称为控制反转;创建被调用者 实例的工作通常由Spring容器来完成,然后注入调用者,因此也称为依赖注入。
- 其实无论是在Spring容器创建People的Bean,还是在主程序中调用People这个Bean都是涉及依赖注入的。例如在创建People的Bean,我需要获取dog和cat的Bean,那就为People的cat和dog属性进行依赖注入cat和dog的Bean。在主方法main中调用People,那也得向People people对象进行依赖注入。
装配是什么?
Bean的装配可以理解为依赖注入,Bean的装配方式,即Bean的依赖注入方式。举个例子,People有两个属性dog和cat,那么就需要将dog和cat两个bean注入到People中。
Spring中bean有三种装配机制
装配方式1:在xml中显式装配
<!--在xml中显式配置-->
<!--csBean类有两个属性:title和author-->
<bean name="cdBean" class="com.my.spring.bean.CDBean">
<property name="title" value="The World!!"/>
<property name="author" value="Mr.D"/>
</bean>
<!--csPlayer类有一个属性:cdBean-->
<!--对csPlayer的属性csBean进行依赖注入,称为Bean装配,或者依赖关系注入-->
<bean name="cdPlayer" class="com.my.spring.service.impl.CDPlayerImpl">
<property name="cdBean" ref="cdBean"/>
</bean>
装配方式2:在java中显式装配:都需要在Config配置类重写
//1. 注册Bean
/*
@Configuration注解的作用:声明一个类为配置类,用于取代bean.xml配置文件注册bean对象。
@Configuration(启动容器) 等同于spring的配置文件xml里的<beans>标签;
*/
@Configuration
class Config {
@Bean //@Bean(注册Bean) 等同于spring的配置文件xml里面的<bean>标签。
public Seat seat(){ return new Seat();}
}
//2. 获取Bean
@Bean //通过set方法注入bean
public Car car(){
Car car = new Car();
car.setSeat(seat());
car.setEngine(engine());
car.setWheel(wheel());
return car;
}
@Bean //通过构造器方法获取
public Car anotherCar(Wheel wheel, Seat seat, Engine engine){
Car car = new Car();
car.setSeat(seat);
car.setEngine(engine);
car.setWheel(wheel);
return car;
}
装配方式3:隐式的bean发现机制和自动装配
- 显示装配与自动装配的区别
显式装配:直接指定依赖项的名称,非常明显和确定,所以称之为显式装配
自动装配:由Spring容器自动的将符合指定类型或指定名称的依赖项注入到bean的属性中
11、SpringBoot、SpringMVC、Spring有什么区别
-
Spring
Spring是一个IOC容器,用来管理Bean。
Spring提供AOP机制弥补OOP代码重复问题 -
SpringMVC
SpringMVC是Spring对web框架的一个解决方法,提供了一个总的前端控制器Servlet,用来接收请求. -
SpringBoot
SprintBoot是Spring提供的一个快速开发工具包,让程序员更方便、更快死的开发Spring+SpringMVC框架。
12、SpringMVC工作流程
SpingMVC的常用组件
1)DispatcherServlet
- 是一种前端控制器,由框架提供。
- 作用:统一处理请求和响应。除此之外还是整个流程控制的中心,由 DispatcherServlet 来调用其他组件,处理用户的请求
2)HandlerMapping
- 处理器映射器,由框架提供。
- 作用:根据请求的 url、method 等信息来查找具体的 Handler(一般来讲是Controller)
3)HandlerAdapter
- 处理器适配器 ,由框架提供。因为SpringMVC中的Handler可以是任意形式,只要能处理请求就可以,但是Servlet需要的处理方法的结构却是固定的,都是以request和response为参数的方法。如何让固定的Servlet处理方法调用灵活的Handler来进行处理呢?这就是HandlerAdapter要做的事情。
- 作用:根据映射器找到的处理器 Handler 信息,按照特定的规则去执行相关的处理器 Handler。
4)Handler
- 处理器,注意,这个需由工程师自己开发。一般来讲是Controller,在Controller层中的@RequestMapping标注的所有方法都可以看成是一个Handler,只要可以实际处理请求就可以是Handler
- 作用:在 DispatcherServlet 的控制下,Handler对具体的用户请求进行处理
5)ViewResolver
- 视图解析器,由框架提供。
- 作用: ViewResolver 负责将处理结果生成 View 视图:ViewResolver 首先根据逻辑视图名解析成物理视图名,即具体的页面地址,再生成 View 视图对象,最后对 View 进行渲染将处理结果通过页面展示给用户。
6)View
- 视图,工程师自己开发
- 作用:View接口的职责就是接收model对象、Request对象、Response对象,并渲染输出结果给Response对象。
SpringMVC的工作流程
13、Java SPI
让你彻底明白Java SPI,内附实例代码演示
14、SpringBoot自动配置原理
SpringBoot的自动配置
15、SpringBoot的starter
简述starter:
- 在使用spring+springMVC的时候,如果需要引入mybatis等框架,需要到xml定义mybatis需要的bean,这样太麻烦了,于是就出现了starter。
- starter就是定义一个starter的jar包,写一个@Configuration配置类,将这些bean定义在里面,然后在starter包的META-IF/spring.factories中写该配置类,springboot会按照约定来加载配置类
- 开发人员只需要将相应的starter包依赖导入进应用,进行相应的属性配置(yml文件),就可以直接进行代码开发,使用对应的功能了,比如mybatis-spring-starter,spring-boot-starter-redis