为什么使用线程池,优势是什么?
线程的创建和销毁是比较重且好资源的操作,Java线程依赖于内核线程,创建线程需要进行操作系统状态切换,为避免过度的消耗资源。需要想办法重用线程去执行多个任务,就可以利用线程池技术解决。
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。 他的主要特点为: 线程复用; 控制最大并发数; 管理线程。
线程池优势: 1、重用存在的线程,减少线程创建和销毁的开销,提高性能(陈低资源消耗) 2、提高响应速度,当任务达到时,任务可以不需要等待线程创建就能立即执行(提高响应速度) 3、提高线程的可管理性,统一对线程进行分配、调优和监控(增强可管理性)
一、线程池工作原理?
-
线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面
有任务,线程池也不会马上执行它们。
-
当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要
创建非核心线程立刻运行这个任务;
d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池
会抛出异常 RejectExecutionException。
-
当一个线程完成任务时,它会从队列中取下一个任务来执行。
-
当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运
行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它
最终会收缩到 corePoolSize 的大小。
二、线程池如何使用?
Excutors可以创建线程池的常见4种方式:
-
newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按
照任务的提交顺序执行。
-
newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线
程达到线程池的最大大小。
-
newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线
程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
-
newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行
任务的需求。
企业最佳实践:不要使用Executors直接创建线程池,会出现OOM问题,要使用ThreadPoolExecutor创建
引用自《阿里巴巴开发手册》
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这 样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下: 1) FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2) CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
创建线程池方式一:new ThreadPoolExecutor 方式
ExecutorService executorService = new ThreadPoolExecutor(3,5,10, TimeUnit.SECONDS,new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); for (int i = 0; i < 9; i++) { executorService.execute(()->{ System.out.println(Thread.currentThread().getName() + "开始办理业务了。。。。。。"); }); }
创建线程池方式二:spring的ThreadPoolTaskExecutor方式
@Configuration public class ExecturConfig { @Bean("taskExector") public Executor taskExector() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(3);//核心池大小 executor.setMaxPoolSize(5);//最大线程数 executor.setQueueCapacity(3);//队列长度 executor.setKeepAliveSeconds(10);//线程空闲时间 executor.setThreadNamePrefix("tsak-asyn");//线程前缀名称 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());//配置拒绝策略 return executor; } }
三、线程池重要参数有哪些?
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
-
corePoolSize => 线程池核心线程数量
-
maximumPoolSize => 线程池最大数量(包含核心线程数量)
-
keepAliveTime => 当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多
次时间内会被销毁。
-
unit => keepAliveTime 的单位
-
workQueue => 线程池所使用的缓冲队列,被提交但尚未被执行的任务
-
threadFactory => 线程工厂,用于创建线程,一般用默认的即可
-
handler => 拒绝策略,当任务太多来不及处理,如何拒绝任务
四、线程池中会用到哪些队列?
-
ArrayBlockQueue(基于数组的有界阻塞队列),防止资源耗尽问题
-
LinkedBlockQueue(基于链表的无界阻塞队列,Intger.MAX),此时maximumPoolSize参数无用
-
SynchronousBlockQueue(不存储元素的阻塞队列,当队列有1个元素时,必须被消费才可以再存入)
-
ProprityBlockQueue(基于最小二叉堆实现的优先级队列,属于无界阻塞队列)
-
DelayQueue只有当其指定的延迟时间到了,才能够从队列中获取到该元素
DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
推荐使用有界队列,有界队列有助于避免资源耗尽的情况发生
五、线程池的拒绝策略有哪些?
1、ThreadPoolExecutor.AbortPolicy:丢弃任务抛出RejectedExecutionException异常打断当前执行流程(默认)
使用场景:ExecutorService接口的系列ThreadPoolExecutor因为都没有显示的设置拒绝策略,所以默认的都是这个。ExecutorService中的线程池实例队列都是无界的,也就是说把内存撑爆了都不会触发拒绝策略。当自己自定义线程池实例时,使用这个策略一定要处理好触发策略时抛的异常,因为他会打断当前的执行流程。
2、ThreadPoolExecutor.CallerRunsPolicy:只要线程池没有关闭,就由提交任务的当前线程处理。
使用场景:一般在不允许失败的、对性能要求不高、并发量较小的场景下使用,因为线程池一般情况下不会关闭,也就是提交的任务一定会被运行,但是由于是调用者线程自己执行的,当多次提交任务时,就会阻塞后续任务执行,性能和效率自然就慢了。
3、ThreadPoolExecutor.DiscardPolicy:直接静悄悄的丢弃这个任务,不触发任何动作
使用场景:如果提交的任务无关紧要,你就可以使用它 。因为它就是个空实现,会悄无声息的吞噬你的的任务。所以这个策略基本上不用了
4、ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,重新提交被拒绝的任务
使用场景:这个策略还是会丢弃任务,丢弃时也是毫无声息,但是特点是丢弃的是老的未执行的任务,而且是待执行优先级较高的任务。基于这个特性,我能想到的场景就是,发布消息,和修改消息,当消息发布出去后,还未执行,此时更新的消息又来了,这个时候未执行的消息的版本比现在提交的消息版本要低就可以被丢弃了。因为队列中还有可能存在消息版本更低的消息会排队执行,所以在真正处理消息的时候一定要做好消息的版本比较。
以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际
需要,完全可以自己扩展 RejectedExecutionHandler 接口。
六、线程池状态有哪些?
1.RUNNING 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0
2.SHUTDOWN 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
3.STOP 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
4.TIDYING 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
5.TERMINATED 状态说明:线程池彻底终止,就变成TERMINATED状态。 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
七、核心线程和非核心线程的销毁机制?
核心线程不会销毁,非核心线程要等到keepAliveTime后才销毁
八、线程池内抛出异常,线程池会怎么办?
当线程池中线程执行任务的时候,任务出现未被捕获的异常的情况下,线程池会将允许该任务的线程从池中移除并销毁,且同时会创建一个新的线程加入到线程池中;可以通过ThreadFactory自定义线程并捕获线程内抛出的异常,也就是说甭管我们是否去捕获和处理线程池中工作线程抛出的异常,这个线程都会从线程池中被移除
九、submit和execute方法的区别?
1、参数有区别,都可以是Runnable,submit也可以是Callable 2、submit有返回值,而execute没有 3、submit方便Exception处理
十、线程池如何重用线程的?
1、当Thread的run方法执行完一个任务之后,会循环地从阻塞队列中取任务来执行,这样执行完一个任务之后就不会立即销毁了; 2、当工作线程数小于核心线程数,那些空闲的核心线程再去队列取任务的时候,如果队列中的Runnable数量为0,就会阻塞当前线程,这样线程就不会回收了