1,自我介绍
2,说一下你对spring ioc和aop的理解
ioc
控制反转是就是应用本身不负责依赖对象的创建和维护,依赖对象的创建及维护是由外部容器负责的,所有的类的创建、销毁都由 spring来控制,也就是说控制对象生存周期的不再是引用它的对象,而是spring。这样控制权就有应用转移到了外部容器,控制权的转移就是控制反转。
IoC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI(Dependency Injection,依赖注入)来实现的。比如对象A需要操作数据库,以前我们总是要在A中自己编写代码来获得一个Connection对象,有了 spring我们就只需要告诉spring,A中需要一个Connection,至于这个Connection怎么构造,何时构造,A不需要知道。在系统运行时,spring会在适当的时候制造一个Connection,然后像打针一样,注射到A当中,这样就完成了对各个对象之间关系的控制。
spring就是通过反射来实现注入的。
首先,我们定义一个Bean类,这个类用来存放一个Bean拥有的属性
-
/* Bean Id */
-
private String id;
-
/* Bean Class */
-
private String type;
-
/* Bean Property */
-
private Map<String, Object> properties = new HashMap<String, Object>();
一个Bean包括id,type,和Properties。
接下来Spring 就开始加载我们的配置文件了,将我们配置的信息保存在一个HashMap中,HashMap的key就是Bean 的 Id ,HasMap 的value是这个Bean,只有这样我们才能通过context.getBean("animal")这个方法获得Animal这个类。我们都知道Spirng可以注入基本类型,而且可以注入像List,Map这样的类型,接下来就让我们以Map为例看看Spring是怎么保存的吧
其实依赖注入的思想也很简单,它是通过反射机制实现的,在实例化一个类时,它通过反射调用类中set方法将事先保存在HashMap中的类属性注入到类中。
在Java中依然注入有以下三种实现方式:
- 构造器注入
- Setter方法注入
- 接口注入
IOC容器的初始化分为三个过程实现:
第一个过程是Resource资源定位。这个Resouce指的是BeanDefinition的资源定位。这个过程就是容器找数据的过程,就像水桶装水需要先找到水一样。
第二个过程是BeanDefinition的载入过程。这个载入过程是把用户定义好的Bean表示成Ioc容器内部的数据结构,而这个容器内部的数据结构就是BeanDefition。
第三个过程是向IOC容器注册这些BeanDefinition的过程,这个过程就是将前面的BeanDefition保存到HashMap中的过程。
---------------------
作者:小小旭GISer
来源:优快云
原文:https://blog.youkuaiyun.com/u010723709/article/details/47046211
版权声明:本文为博主原创文章,转载请附上博文链接!
AOP
它利用一种称为"横切"的技术,将那些影响了多个类的公共行为封装到一个可重用模块,简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。比如权限认证、日志、事物等。
Spring提供了两种方式来生成代理对象: JDKProxy和Cglib,
1、JDK动态代理
利用拦截器(拦截器必须实现InvocationHanlder)加上反射机制生成一个实现代理接口的匿名类,
在调用具体方法前调用InvokeHandler来处理。
2、CGLiB动态代理
利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
3、何时使用JDK还是CGLiB?
1)如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP。
2)如果目标对象实现了接口,可以强制使用CGLIB实现AOP。
3)如果目标对象没有实现了接口,必须采用CGLIB库,Spring会自动在JDK动态代理和CGLIB之间转换。
---------------------
作者:街灯下的小草
来源:优快云
原文:https://blog.youkuaiyun.com/yhl_jxy/article/details/80635012
版权声明:本文为博主原创文章,转载请附上博文链接!
Spring有几种配置方式?
- 基于XML的配置
- 基于注解的配置
- 基于Java的配置
Spring框架中的单例bean是线程安全的吗?
不,Spring框架中的单例bean不是线程安全的。
ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。
有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象 ,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。
无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象 .不能保存数据,是不变类,是线程安全的。
无状态的Bean适合用不变模式,技术就是单例模式,这样可以共享实例,提高性能。有状态的Bean,多线程环境下不安全,那么适合用Prototype原型模式。Prototype: 每次对bean的请求都会创建一个新的bean实例。Struts2默认的实现是Prototype模式。也就是每个请求都新生成一个Action实例,所以不存在线程安全问题。
Spring Bean的作用域之间有什么区别?
Spring容器中的bean可以分为5个范围。所有范围的名称都是自说明的,但是为了避免混淆,还是让我们来解释一下:
- singleton:这种bean范围是默认的,这种范围确保不管接受到多少个请求,每个容器中只有一个bean的实例,单例的模式由bean factory自身来维护。
- prototype:原形范围与单例范围相反,为每一个bean请求提供一个实例。
- request:在请求bean范围内会每一个来自客户端的网络请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收。
- Session:与请求范围类似,确保每个session中有一个bean的实例,在session过期后,bean会随之失效。
- global-session:global-session和Portlet应用相关。当你的应用部署在Portlet容器中工作时,它包含很多portlet。如果你想要声明让所有的portlet共用全局的存储变量的话,那么这全局变量需要存储在global-session中。
全局作用域与Servlet中的session作用域效果相同。
请解释Spring Bean的自动装配?
在Spring框架中,在配置文件中设定bean的依赖关系是一个很好的机制,Spring容器还可以自动装配合作关系bean之间的关联关系。这意味着Spring可以通过向Bean Factory中注入的方式自动搞定bean之间的依赖关系。自动装配可以设置在每个bean上,也可以设定在特定的bean上。
下面的XML配置文件表明了如何根据名称将一个bean设置为自动装配:
1 |
|
除了bean配置文件中提供的自动装配模式,还可以使用@Autowired
注解来自动装配指定的
bean
。在使用
@Autowired
注解之前需要在按照如下的配置方式在
Spring
配置文件进行配置才可以使用。
1 |
|
也可以通过在配置文件中配置AutowiredAnnotationBeanPostProcessor
达到相同的效果。
1 |
|
配置好以后就可以使用@Autowired
来标注了。
1 2 3 4 |
|
BeanFactory和ApplicationContext的区别?
BeanFactory
是Spring里面最低层的接口,提供了最简单的容器的功能,只提供了实例化对象和拿对象的功能。
两者装载bean的区别
- BeanFactory:在启动的时候不会去实例化Bean,中有从容器中拿Bean的时候才会去实例化;
- ApplicationContext:在启动的时候就把所有的Bean全部实例化了。它还可以为Bean配置lazy-init=true来让Bean延迟实例化;
作者:搜云库技术团队
链接:https://juejin.im/post/5ab1ed535188257b1c75171c
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
ApplicationContext 上下文的生命周期?
Spring上下文中的Bean也类似,【Spring上下文的生命周期】
- 实例化一个Bean,也就是我们通常说的new;
- 按照Spring上下文对实例化的Bean进行配置,也就是IOC注入
- 如果这个Bean实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,此处传递的是Spring配置文件中Bean的ID;
- 如果这个Bean实现了BeanFactoryAware接口,会调用它实现的setBeanFactory(),传递的是Spring工厂本身(可以用这个方法获取到其他Bean);
- 如果这个Bean实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文,该方式同样可以实现步骤4,但比4更好,以为ApplicationContext是BeanFactory的子接口,有更多的实现方法;
- 如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessBeforeInitialization(Object obj, String s)方法,BeanPostProcessor经常被用作是Bean内容的更改,并且由于这个是在Bean初始化结束时调用After方法,也可用于内存或缓存技术;
- 如果这个Bean在Spring配置文件中配置了init-method属性会自动调用其配置的初始化方法;
- 如果这个Bean关联了BeanPostProcessor接口,将会调用postAfterInitialization(Object obj, String s)方法;
注意:以上工作完成以后就可以用这个Bean了,那这个Bean是一个single的,所以一般情况下我们调用同一个ID的Bean会是在内容地址相同的实例
- 当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean接口,会调用其实现的destroy方法
- 最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法
以上10步骤可以作为面试或者笔试的模板,另外这里描述的是应用Spring上下文Bean的生命周期,如果应用Spring的工厂也就是BeanFactory的话去掉第5步就Ok了;
Spring Bean 的生命周期?
Spring框架中,一旦把一个Bean纳入Spring IOC容器之中,这个Bean的生命周期就会交由容器进行管理,一般担当管理角色的是BeanFactory或者ApplicationContext,认识一下Bean的生命周期活动,对更好的利用它有很大的帮助:
下面以BeanFactory为例,说明一个Bean的生命周期活动。
- Bean的建立, 由BeanFactory读取Bean定义文件,并生成各个实例;
- Setter注入,执行Bean的属性依赖注入;
- BeanNameAware的setBeanName(), 如果实现该接口,则执行其setBeanName方法;
- BeanFactoryAware的setBeanFactory(),如果实现该接口,则执行其setBeanFactory方法;
- BeanPostProcessor的processBeforeInitialization(),如果有关联的processor,则在Bean初始化之前都会执行这个实例的processBeforeInitialization()方法;
- InitializingBean的afterPropertiesSet(),如果实现了该接口,则执行其afterPropertiesSet()方法;
- Bean定义文件中定义init-method;
- BeanPostProcessors的processAfterInitialization(),如果有关联的processor,则在Bean初始化之前都会执行这个实例的processAfterInitialization()方法;
- DisposableBean的destroy(),在容器关闭时,如果Bean类实现了该接口,则执行它的destroy()方法;
- Bean定义文件中定义destroy-method,在容器关闭时,可以在Bean定义文件中使用“destory-method”定义的方法;
SpringMVC流程
- 第一步: 用户发起请求到前端控制器(DispatcherServlet)
- 第二步:前端控制器请求处理器映射器(HandlerMappering)去查找处理器(Handle):通过xml配置或者注解进行查找
- 第三步:找到以后处理器映射器(HandlerMappering)像前端控制器返回执行链(HandlerExecutionChain)
- 第四步:前端控制器(DispatcherServlet)调用处理器适配器(HandlerAdapter)去执行处理器(Handler)
- 第五步:处理器适配器去执行Handler
- 第六步:Handler执行完给处理器适配器返回ModelAndView
- 第七步:处理器适配器向前端控制器返回ModelAndView
- 第八步:前端控制器请求视图解析器(ViewResolver)去进行视图解析
- 第九步:视图解析器像前端控制器返回View
- 第十步:前端控制器对视图进行渲染
- 第十一步:前端控制器向用户响应结果
Spring 的不同事务传播行为有哪些,干什么用的?
当事务方法被另一个事务方法调用时, 必须指定事务应该如何传播. 例如: 方法可能继续在现有事务中运行, 也可能开启一个新事务, 并在自己的事务中运行. 事务的传播行为可以由传播属性指定.
Spring 定义了7 种类传播行为.(一二两种最常用)
- ==PROPAGATION_REQUIRED==: 如果存在一个事务,则支持当前事务,如果没有事务则开启。
- ==PROPAGATION_REQUIRES_NEW==: 总是开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起。
- PROPAGATION_SUPPORTS: 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行
- PROPAGATION_MANDATORY: 如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。
- PROPAGATION_NOT_SUPPORTED: 总是非事务地执行,并挂起任何存在的事务。
- PROPAGATION_NEVER: 总是非事务地执行,如果存在一个活动事务,则抛出异常
- PROPAGATION_NESTED:如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行
原子性(Atomicity)
- 原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。
一致性(Consistency)
- 一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。
拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
隔离性(Isolation)
- 隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。
关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。
持久性(Durability)
- 持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
事务的隔离级别
- ISOLATION_DEFAULT: 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别.另外四个与JDBC的隔离级别相对应
- ISOLATION_READ_UNCOMMITTED: 这是事务最低的隔离级别,它充许令外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。
- ISOLATION_READ_COMMITTED: 保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据
- ISOLATION_REPEATABLE_READ: 这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了避免下面的情况产生(不可重复读)。
- ISOLATION_SERIALIZABLE 这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读外,还避免了幻像读。
- 事务的隔离级别要得到底层数据库引擎的支持, 而不是应用程序或者框架的支持.
- Oracle 支持的 2 种事务隔离级别:READ_COMMITED , SERIALIZABLE
- Mysql 支持 4 中事务隔离级别.
事务回滚属性
- 默认情况下只有未检查异常(RuntimeException和Error类型的异常)会导致事务回滚. 而受检查异常不会.
- 事务的回滚规则可以通过@Transactional 注解的 rollbackFor 和 noRollbackFor 属性来定义,这两个属性被声明为 Class[] 类型的, 因此可以为这两个属性指定多个异常类。
- rollbackFor: 遇到时必须进行回滚
- noRollbackFor: 一组异常类,遇到时必须不回滚
- 编程式事务管理:Spring推荐使用TransactionTemplate,实际开发中使用声明式事务较多。
- 声明式事务管理:将我们从复杂的事务处理中解脱出来,获取连接,关闭连接、事务提交、回滚、异常处理等这些操作都不用我们处理了,Spring都会帮我们处理。
声明式事务管理使用了AOP面向切面编程实现的,本质就是在目标方法执行前后进行拦截。在目标方法执行前加入或创建一个事务,在执行方法执行后,根据实际情况选择提交或是回滚事务。
声明式事务的优缺点:
- 优点:不需要在业务逻辑代码中编写事务相关代码,只需要在配置文件配置或使用注解(@Transaction),这种方式没有侵入性。
- 缺点:声明式事务的最细粒度作用于方法上,如果像代码块也有事务需求,只能变通下,将代码块变为方法。
说一说自己对于 synchronized 关键字的了解
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
- 修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
- 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
synchronized 关键字底层原理属于 JVM 层面。
① synchronized 同步语句块的情况
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
② synchronized 修饰方法的的情况
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
①偏向锁
引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉。
偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!关于偏向锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。
但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
② 轻量级锁
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。关于轻量级锁的加锁和解锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。
轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!
③ 自旋锁和自适应自旋
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。
一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋。
1.5 谈谈 synchronized和ReenTrantLock 的区别
可重入性:
从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入的,两者关于这个的区别不大。两者都是同一个线程没进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
锁的实现:
Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。
性能的区别:
在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。
功能区别:
便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。ReenTrantLock 比 synchronized 增加了一些高级功能
锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized
ReenTrantLock独有的能力:
相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
1. ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
2. ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
3. ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
二 线程池常见面试题
1.线程池的最最基本的原理
线程池,说白了就是为了避免频繁创建和销毁线程带来的巨大开销,就维护一定数量的线程池,创建以后别销毁,用完了扔回去再个下一个任务来使用,这样就可以提高线程的使用效率。一般常见于后台处理类的系统,或者是复杂接口中的多数据并发获取。
Java线程池包含4个部分
(1)线程池管理器(ThreadPool):就是负责创建和销毁线程池的
(2)工作线程(PoolWorker):就是线程池中的一个线程
(3)工作任务(Task):这个就是线程池里的某个线程需要执行的业务代码,这个是你自己编写的业务逻辑
(4)任务队列(TaskQueue):这个是扔到线程池里的任务需要进行排队,要进任务队列
2.常用的几种线程池和API
(1)SingleThreadExecutor(单线程线程池,很少用 -> 自己做一个内存队列 -> 启动后台线程去消费)
(2)FixedThreadExecutor(固定数量线程池):比如说,线程池里面固定就100个线程,超过这个线程数就到队列里面去排队等待
(3)CachedThreadExecutor(自动回收空闲线程,根据需要自动新增线程,传说中的无界线程池):无论有多少任务,根据你的需要,无限制的创建任意多的线程,在最短的时间内来满足你,但是高峰过去之后,如果有大量的线程处于空闲状态,没有活儿可以干,等待60s之后空闲的线程就被销毁了
(4)ScheduledThreadExecutor(线程数量无限制,支持定时调度执行某个线程):提交一个任务,对于这个任务不是立马执行的,是可以设定一个定时调度的逻辑,比如说每隔60s执行一次,这个一般不用,一般来说就用spring schedule的支持
一般其实最常用的是FixedThreadExecutor和CachedThreadExecutor
ScheduleThreadExecutor,也可能会使用,但是就是除非你的那个线程任务要定时调度,才会用这个线程池,不过说实话,简单的定时调度一般就是走spring的schedule支持就行了,当然如果你要用这个也行
Java的线程池比较重要的几个API
(1)Executor:代表线程池的接口,有个execute()方法,扔进去一个Runnable类型对象,就可以分配一个线程给你执行
(2)ExecutorService:这是Executor的子接口,相当于是一个线程池的接口,有销毁线程池等方法 -> ExecutorService就代表了一个线程池管理器,会负责管理线程池 -> 线程的创建和销毁 -> 队列排队
(3)Executors:线程池的辅助工具类,辅助入口类,可以通过Executors来快捷的创建你需要的线程池。创建线程池的入口类,包含newSingleThreadExecutor()、newCachedThreadPool()、newScheduleThreadPool()、newFixedThreadPool(),这些方法,就是可以让你创建不同的线程池出来
(4)ThreadPoolExecutor:这是ExecutorService的实现类,这才是正儿八经代表一个线程池的类,一般在Executors里创建线程池的时候,内部都是直接创建一个ThreadPoolExecutor的实例对象返回的,然后同时给设置了各种默认参数。
如果我们要创建一个线程池,两种方式,要么就是Executors.newXX()方法,快捷的创建一个线程池出来,线程池的所有参数设置都采取默认的方式;要么是自己手动构建一个THreadPoolExecutor的一个对象,所有的线程池的参数,都可以自己手动来调整和设置
public class Executors {
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
}
如果我们要用线程池,大部分同学,我估计如果之前使用过线程池的话,一遍都是怎么用的呢?
public class MyTask implements Runnable {
public void run() {
// 实现你的业务逻辑
// 这段业务逻辑,就会交给线程池里的某个线程去执行
}
}
// 创建一个固定线程数量为100的FixedThreadPool线程池
ExecutorService myThreadPool = Executors.newFixedThreadPool(100);
// 要往线程池里提交一个任务
Runnable myTask = new MyTask();
myThreadPool.execute(myTask);
// 上面的代码就是平时大家最最常用的线程池的使用代码
// 执行了线程池的execute()方法,就相当等于是给这个线程提交了一个任务
// 线程池会优先用有已有的线程来处理这个任务
// 但是如果所有的线程池里的线程都处于一个繁忙的状态,此时就会将这个任务扔到队列里去排队,等待某个线程空闲之后来处理这个任务
到此为止,都是讲的很easy的,但是你会发现只是知道这么点是完全不够用的
3.3 线程池的构造参数和真正的工作原理
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
ThreadPoolExecutor就是线程池,那么这个类的构造函数的所有入参,就是你可以设置的参数,我们来解释一下这些参数吧
corePoolSize:线程池里的核心线程数量
maximumPoolSize:线程池里允许有的最大线程数量
keepAliveTime:如果线程数量大于corePoolSize的时候,多出来的线程会等待指定的时间之后就被释放掉,这个就是用来设置空闲线程等待时间的
unit:这个是上面那个keepAliveTime的单位
workQueue:这个是说,通过ThreadPoolExecutor.execute()方法扔进来的Runnable工作任务,会进入一个队列里面去排队,这就是那个队列
threadFactory:如果需要创建新的线程放入线程池的时候,就是通过这个线程工厂来创建的
handler:假如说上面那个workQueue是有固定大小的,如果往队列里扔的任务数量超过了队列大小,咋办?就用这个handler来处理,AbortPolicy、DiscardPolicy、DiscardOldestPolicy,如果说线程都繁忙,队列还满了,此时就会报错,RejectException
这些参数的含义先解释一下:
假设我们自己手动创建一个ThreadPoolExecutor线程池,设置了以下的一些参数
corePoolSize:2个
mamximumPoolSize:4个
keepAliveTime:60s
workQueue:ArrayBlockingQueue,有界阻塞队列,队列大小是4
handler:默认的策略,抛出来一个ThreadPoolRejectException
(1)一开始线程池里的线程是空的,一个都没有。有一个变量维护的是当前线程数量,这个变量是poolSize,poolSize = 0,如果当前线程的数量小于corePoolSize(2),poolSize < corePoolSize,那么来了一个任务优先创建线程,直到线程池里的线程数量跟corePoolSize一样;poolSize = 1,poolSize < corePoolSize(2),又创建一个线程来处理这个任务;poolSize = 2
(2)如果当前线程池的线程数量(poolSize = 2)大于等于corePoolSize(2)的时候,而且任务队列没满(最大大小是4,但是当前元素数量是0),那么就扔到任务队列里去
(3)如果当前线程池的线程数量大于等于corePoolSize的时候,而且任务队列满了(最大大小是4,当前已经放了4个元素了,已经满了),那么如果当前线程数量小于最大线程数(poolSize = 2,maimumPoolSize = 4,poolSize < maximumPoolSize),就继续创建线程;poolSize = 3,提交了一个任务,poolSize >= corePoolSize,任务队列满,poolSize < maximumPoolSize,再次创建一个任务
(4)如果此时poolSize >= corePoolSize,任务队列满,poolSize == maximumPoolSize,此时再次提交一个任务,当前线程数已经达到了最大线程数了,那么就使用handler来处理,默认是抛出异常,ThreadPoolRejectExeception
(5)此时线程池里有4个线程,都处于空闲状态,corePoolSize指定的是就2个线程就可以了,但是此时超过了corePoolSize 2个线程,所以如果那超出的2个线程空闲时间超过了60s,然后线程池就会将超出的2个线程给回收掉
如何设置池的这些参数?先来看看创建线程池的默认代码
其实上面都说过了,啥时候会创建新线程?其实就是线程数没到corePoolSize的时候,会创建线程;接着就是任务队列满了,但是线程数小于maximumPoolSize的时候,也会创建线程;创建的时候通过threadFactory来创建即可
3.4 常用线程池的工作原理
FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
(1)corePoolSize = 100,maximumPoolSize = 100,keepAliveTime = 0,workQueue = 无界队列
(2)刚开始的时候,比如说假设一开始线程池里没有线程,你就不断的提交任务,瞬间提交了100个任务,一下子创建100个线程出来,其实poolSize == corePoolSize,再提交任务,直接就会发现LinkedBlockQueue根本就没有大小的限制,所以说根本就不会满,所以此时后续的所有任务直接扔到LinkedBlockingQueue里面去排队
(3)100个线程,只要出现了空闲,就会从队列里面去获取任务来处理,以此类推,就100个线程,不停的处理任务
- LinkedBlockingQueue根本就不会满,直到扔任务扔的内存溢出,扔了几百万个任务,几千万个任务,队列实在是太大太大了,导致内存溢出,满了,就死了
- maximumPoolSize根本就没用,而且其实也是直接跟corePoolSize设置成一样
- keepAliveTime = 0,也就是,创建出来的线程,根本就不会去判断是否超过了指定的空闲时间,不会去回收空闲线程数量,后面就维护这么固定数量的一个线程,有任务就往队列里面怼,固定数量的比如100个线程不停的处理任务,如果100个线程都处理不过来,那么就无限制的往LinkedBlockingQueue里面去排队,直到内存溢出
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- corePoolSize = 0,maximumPoolSize = 最大值(无限大),keepAliveTime = 60s,workQueue=SynchronousQueue
- SynchronousQueue(实际上没有存储数据的空闲,是用来做多线程通信之间的协调作用的),一开始提交一个任务过来,要求线程池里必须有一个线程对应可以处理这个任务,但是此时一个线程都没有,poolSize >= corePoolSize , workQueue已经满了,poolSize < maximumPoolSize(最大值),直接就会创建一个新的线程来处理这个任务
- 如果短期内有大量的任务都涌进来,实际上是走一个直接提交的思路,对每个任务,如果没法找到一个空闲的线程来处理它,那么就会立即创建一个新的线程出来,来处理这个新提交的任务
- 短时间内,如果大量的任务涌入,可能会导致瞬间创建出来几百个线程,几千个线程,是不固定的
- 但是当这些线程工作完一段时间之后,就会处于空闲状态,就会看超过60s的空闲,就会直接将空闲的线程给释放掉
总结一下:
(1)FixedThreadPool:线程数量是固定的,如果所有线程都繁忙,后续任务全部在一个无界队列里面排队,无限任务来排队,直到内存溢出
(2)CachcedThreadPool:线程数量是不固定的,如果一下子涌入大量任务,没有空闲线程,那么就创建新的线程来处理;如果有空闲线程,就是一个任务配对一个空闲线程来处理;如果线程空闲时间超过60s,就给回收掉空闲线程
3.5 各种线程池在什么样的场景下使用
FixedThreadPool:比较适用于什么场景呢?负载比较重,而且负载比较稳定的这么一个场景,我给大家来举个例子,我们之前线上有一套系统,负载比较重,后台系统,每分钟要执行几百个复杂的大SQL,就是用FixedThreadPool是最合适的。
因为负载稳定,所以一般来说,不会出现说突然瞬间涌入大量的请求,100个线程处理不过来,然后就直接无限制的排队,然后oom内存溢出,死了
CachedThreadPool:负载很稳定的场景,用CachedThreadPool就浪费了;每天大部分时候可能就是负载很低的,CachedThreadPool,用少量的线程就可以满足低负载,不会给系统引入太大的压力;但是每天如果有少量的高峰期,比如说中午或者是晚上,高峰期可能需要一下子搞几百个线程出来,那么CachedThreadPool就可以满足这个场景;高峰期应付过去之后,线程如果处于空闲状态超过60s,自动回收空闲线程,避免给系统带来过大的负载
3.6 如何设置线程池的参数
这是Executors自己创建的,其实无非就是实例化一个ThreadPoolExecutor对象,你要是不想用Executors创建,就自己构造一个ThreadPoolExecutor对象也行,那么参数你就自己设置不就得了
通常来说,建议大家就用Executors提供的默认的线程池就可以了,我觉得还是挺合适的,因为他们的默认的参数设置可以满足大部分的场景了,但是如果你在学习了这一课之后,确实需要自己去动手定制一下线程池的策略,那么就自己动手构建ThreadPoolExecutor实例就可以了,所有的参数自己设置
先来看看默认的参数设置,其实固定大小的线程池,说白了,就是corePoolSize和maximumPooSize是一样的,那么只要达到了你传入的那个线程数量,而且任务队列满了,就报错;cached线程池,说白了,就是可以无限的创建线程,因为maximumPooSize是无限大的,但是超过60秒空闲就给你回收
如果你要自己创建个线程池,一般自己设置参数好了:
corePoolSize:这个其实就是说你算一下每个任务要耗费多少时间,比如一个任务大概100ms,那么每个线程每秒可以处理10个任务,然后你算算你每秒总共要处理多少个任务啊,比如说200个任务,那么你就需要20个线程,每个线程每秒处理10个任务,不就可以处理200个任务。但是一般都会多设置一些,比如你可以设置个30个线程。
坦白来讲,如果你面试被问到这个问题,体现你水平的地方,其实在于不同场景的理解:
你希望用类似于FixedThreadPool的这个线程池,corePoolSize和maximumPoolSize按照上面说的策略设置成一样的就可以了
如果用的是FixedPool的话,一般在于workQueue和handler的理解,因为你看下默认的实现,其实线程数量达到corePoolSize的时候,就会放入workQueue排队,但是默认使用的是无界队列,LinkedBlockingQueue,所以会无限制往里面排队,然后就是你corePooSize指定数量的线程不断的处理,队列里的任务可能会无限制的增加
这个其实就是适合处理那种长期不断有大量任务进来,长期负载都很重,所以你不能用CachedPool,否则长期让机器运行大量线程,可能导致机器死掉,cpu耗尽。所以你就只能控制线程的数量,用有限的线程不断的处理源源不断进入的任务,有时高峰时任务较多,就做一下排队即可。
所以FixedPool的参数里,对于workQueue,你要考虑一点,默认的是无界队列,可能会有问题,就是要是无限排队,别把机器给搞死了,那么这个时候你可以换成ArrayBlockingQueue,就是有界队列,自己设置一个最大长度,一旦超出了最大长度,就通过handler去处理,你可以自己对handler接口实现自己的逻辑,我给你举个例子,此时你可以把数据放到比如数据库里去,做离线存储或者是什么的
需要去实现CachedThreadPool的这么一个策略
corePoolSize可以设置为0,但是maximumPoolSize考虑不用设置成无限大,有一个风险,假设突然进来的流量高峰,导致你的线程池一下子出来了几万个线程,瞬间会打满cpu负载,直接机器会死掉
maximumPoolSize可以设置成一个,你的机器cpu能负载的最大的线程数,一个经验值,4核8G的虚拟机,你线程池启动的线程数量达到100个就差不多了,如果同时有100个线程,而且做很频繁的操作,cpu可能就快到70%,80%,90%
corePoolSize = 0,maximumPoolSize = 150 -> handler报错 -> 实现一个handler,将多余的线程给离线存储起来,后续高峰过了,再重新扫描出来重新提交
你看下CachedPool,他那里用的是SynchronousQueue,这个queue的意思是如果要插入一个任务,必须有一个任务已经被消费掉了,所以很可能出现说,线程数量达到corePoolSize之后,大量的任务进来,此时SynchronousQueue里的任务如果还没被拿走,那么就会认为队列满了,此时就会创建新的线程,但是maximumPoolSize是无限大的,所以会无限制的创建新的线程。但是如果后续有线程空闲了,那么就会被回收掉。
所以如果你用CachedPool,相当于是在高峰期,无限制的创建线程来拼命耗尽你的机器资源来处理并发涌入的大量的任务
所以CachedPool,可以用在那种瞬时并发任务较高,但是每个任务耗时都较短的场景,就是短时间内突然来个小高峰,那么就快速启动大量线程来处理,但是每个线程处理都很快,而且高峰很快就过去了