线程池学习
一、概念
线程池:一个容纳多个线程的容器,其中的线程可以反复使用,省去频繁创建线程对象的操作,无需反复创建线程消耗过多资源。
二、线程池的好处
1.降低资源消耗:减少创建销毁线程次数
2.提高相应速度:任务到达后无需创建线程可以立即执行
3.提高线程可管理性:根据系统承受压力,调整线程池中工作线程的数量,防止消耗内存过多,让服务器宕机。(每个线程大概消耗1M内存)
三、线程池的配置
1.实际使用创建线程池的方式:单一、可变、定长都不可用,用自定义线程池。
原因:FixedThreadPool和SingleThreadExecutor底层都是用LinkedBlockingQueue实现的,队列最大长度是Integer.MAX_VALUE,显然会导致OOM。实际使用ThreadPoolExecutor的7个参数,自定义线程池。
2.线程池创建的七个参数
corePoolSize | 线程池常驻核心线程数 |
maximumPoolSize | 能够容纳的最大线程数 |
keepAliveTime | 空闲线程存活时间 |
unit | 存活时间单位 |
workQueue | 存放提交但未执行任务的队列 |
threadFactory | 创建线程的工厂类 |
handler | 等待队列满后的拒绝策略 |
3.工作原理:
新任务到达→如果正在运行的线程数小于corePoolSize
,创建核心线程;大于等于corePoolSize
,放入等待队列。
如果等待队列已满,但正在运行的线程数小于maximumPoolSize
,创建非核心线程;大于等于maximumPoolSize
,启动拒绝策略。当一个线程无事可做一段时间keepAliveTime
后,如果正在运行的线程数大于corePoolSize
,则关闭非核心线程。
4.线程池的拒绝策略
当等待队列满时,且达到最大线程数,再有新任务到来,就需要启动拒绝策略。JDK提供了四种拒绝策略
,分别是。
- AbortPolicy:默认的策略,直接抛出
RejectedExecutionException
异常,阻止系统正常运行。 - CallerRunsPolicy:既不会抛出异常,也不会终止任务,而是将任务返回给调用者。
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交任务。
- DiscardPolicy:直接丢弃任务,不做任何处理。
拒绝策略默认是超出后抛出RejectedExecutionException异常并直接丢弃。
5.corePoolSize和maxiPoolSize 的该如何设置呢?
在设置这两个值之前,首先需要通过任务类型对线程池进行分类, 可以分为IO密集型任务,CPU 密集型任务和混合型任务。
(1)IO密集型任务:主要执行IO操作,由于IO操作时间较长,导致CPU利用率不高,CPU常处于空闲状态。因此线程数通常为CPU核心数2倍。
IO密集型任务要点:允许核心线程数销毁(allowCoreThreadTimeOut(true))、使用有界队列(根据具体需求增加)、优先创建线程而不是加入阻塞队列
(2)CPU密集型任务:主要执行计算任务,CPU利用率高。虽然CPU密集型任务可以并行执行,但并行任务越多,花在任务切换上的时间越多,CPU效率越低。因此CPU密集型任务的线程数等于CPU核心数
(3)混合型任务
任务既要执行计算又要执行IO。相对来说IO操作的执行时间较长,CPU利用率也不是很高。例如WEB服务器的http请求,一次请求包括DB操作、缓存操作等多种操作。
混合型任务有一个计算公式:最佳线程数=(线程等待时间/线程cpu时间+1)*cpu核数
从公式得出: 等待时间所占的比例越高,需要的线程数就越多;cpu所占的比例越高,需要的线程就越少。
公式只是一个理论值,具体还需要结合硬件环境和网络环境不断尝试,获取一个最优值。
四、线程池的使用
1.执行任务
execute和submit的区别
execute和submit都属于线程池的方法,execute只能提交Runnable类型的任务
submit既能提交Runnable类型任务也能提交Callable类型任务。
execute()没有返回值
submit有返回值,所以需要返回值的时候必须使用submit
如果想知道什么时候执行完可搭配CountDownLatch,主线程countDownLatch.await()等待即可。也可以使用待返回参数的Fulture.get()处理
2.开启异步线程
@Autowired ThreadPoolTaskExecutor commonTaskExecutor; //会去匹配 @Bean("commonTaskExecutor") 这个线程池 如果是使用的@Async注解,只需要在注解里面指定bean的名称就可以切换到对应的线程池去了。如下所示: @Async("commonTaskExecutor") public void hello(String name){ logger.info("异步线程启动 started."+name); }
总结@Async可能失效的原因
1.@SpringBootApplication启动类当中没有添加@EnableAsync注解。
2.异步方法使用注解@Async的返回值只能为void或者Future。
3.没有走Spring的代理类。因为@Transactional和@Async注解的实现都是基于Spring的AOP,而AOP的实现是基于动态代理模式实现的。那么注解失效的原因就很明显了,有可能因为调用方法的是对象本身而不是代理对象,因为没有经过Spring容器管理。
解决方法:
1、注解的方法必须是public方法。
2、注解的方法不要定义为static
3、方法一定要从另一个类中调用,也就是从类的外部调用,类的内部调用是无效的。
4、如果需要从类的内部调用,需要先获取其代理类。