该文章为面试精华版,如果是初学者,建议学习专栏:Java并发专栏
文章目录
一、ThreadPoolExecutor参数含义
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize:线程池始终线程数,即使有些是空闲的。设置
allowCoreThreadTimeOut
参数为true,才会进行回收。 - maximumPoolSize:线程池最大线程数,表示在线程池中最多能创建多少个线程。如果当线程池中的数量到达这个数字时,新来的任务会抛出异常。
- keepAliveTime:表示线程没有任务执行时最多能保持多少时间会回收,然后线程池的数目维持在corePoolSize。
- unit:参数keepAliveTime的时间单位
- workQueue:一个阻塞队列,所有的任务都会先放在这里,务;如果对当前对线程的需求超过了corePoolSize大小,会用来存储等待执行的任。
- threadFactory:线程工厂,主要用来创建线程,比如指定线程的名字。
- handler:如果线程池已满,新的任务处理方式。
注意一点:<mark>初始化线程池时,线程数为0</mark>
1. 什么时候创建新的线程?
- 线程初始化时线程数为0
- 当前线程数小于corePoolSize时,提交任务会直接创建新的线程
- 当前线程数大于等于为corePoolSize时,提交任务会放到阻塞队列中,当阻塞队列满时会创建线程
2. 如何关闭线程池?
shutdown(高安全低响应)
-
本质上执行的是
interrupt
方法 -
阻止新来的任务提交,会将线程池的状态改成SHUTDOWN,当再将执行execute提交任务时,如果测试到状态不为RUNNING,则执行拒绝策略,从而达到阻止新任务提交的目的。
-
对已经提交了的任务不会产生任何影响,当已经提交的任务执行完后,它会将那些闲置的线程进行中断,这个过程是异步的,也就是说只会打断空闲线程,如果当前还有任务队列还有任务未执行,线程将继续把任务执行完。
shutdownNow(低安全高响应)
-
阻止新来的任务提交,将线程池的状态改成STOP,当再将执行execute提交任务时,如果测试到状态不为RUNNING,则抛出rejectedExecution,从而达到阻止新任务提交的目的.
-
会中断空闲进程,同时也会中断当前正在运行的线程,即workers中的线程。
-
另外它还将workQueue中的任务给移除,并将这些任务添加到列表中进行返回。
-
如遇已经激活的任务,并且处于阻塞状态时,shutdownNow()会执行1次中断阻塞的操作,此时对应的线程报InterruptedException,如果后续还要等待某个资源,则按正常逻辑等待某个资源的到达。例如,一个线程正在sleep状态中,此时执行shutdownNow(),它向该线程发起interrupt()请求,而sleep()方法遇到有interrupt()请求时,会抛出InterruptedException(),并继续往下执行。在这里要提醒注意的是,在激活的任务中,如果有多个sleep(),该方法只会中断第一个sleep(),而后面的仍然按照正常的执行逻辑进行。
高安全低响应体现在shutdown等待任务执行完成再关闭,可以保证任务一定被执行,但是关闭线程池需要等待较长的时间
低安全高响应体现在shutdownNow会关闭正在执行任务的线程,任务可能并没有执行完毕,也不会回退到任务队列中,将会消失,但是关闭线程池不需要等待较长的时间
如果一个任务执行时间很长,导致线程池长时间关闭不了,可以在创建线程的时候将其设置为守护线程,此时被守护的线程是主线程,只要主线程执行完成,线程池就会强制关闭,可以配合awaitTermination使用:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,2,
30, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5), r -> {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
},new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPoolExecutor = bulidThreadPoolExecutor();
threadPoolExecutor.shutdown();
threadPoolExecutor.awaitTermination(5,TimeUnit.SECONDS);
System.out.println("强制关闭");
}
二、拒绝策略
线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也
塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK 内置的拒绝策略如下:
- AbortPolicy : 直接抛出异常,阻止系统正常运行。
- CallerRunsPolicy : 导致该方法直接在调用者的主线程中执行,而不是在线程池中执行。从而导致主线程在该任务执行结束之前不能提交任何任务。从而有效的阻止了任务的提交。
- DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,会直接出队,并尝试再
次提交当前任务。 - DiscardPolicy :默认情况下它将丢弃被拒绝的任务
以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际
需要,完全可以自己扩展 RejectedExecutionHandler 接口。
三、线程池的状态
- RUNNING:能够接收新任务,以及对已添加的任务进行处理。
- SHUTDOWN:不接收新任务,但能处理已添加的任务。
- STOP:程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
- TIDYING:当所有的任务已终止
- TERMINATED:线程池彻底终止,就变成TERMINATED状态。
四、线程池分类
Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而
只是一个执行线程的工具。真正的线程池接口是 ExecutorService
摘自阿里巴巴开发手册:
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
newCachedThreadPool
- 线程池的参数:coreSize线程数0,最大线程数无限制,线程的允许空闲时间是60s,阻塞队列是SynchronousQueue
- 这个线程池适用情况是
短任务
情况。 - 采用SynchronousQueue,每当提交一个任务,都会超过阻塞队列的长度,导致创建线程,所以说:每当提交一个任务,都会创建一个线程,可能造成OOM。
- 当线程空闲1分钟就会,销毁,所以该线程池会频繁的创建和销毁线程,最终会线程池会自己销毁
newFixedThreadPool
- coreSize和最大线程数都是用户输入的,阻塞队列用的LinkedBlockingQueue,线程的允许空闲时间是0s
- 该线程池的线程数是用户自定义的,不会增加,不会减少,线程池不会自己销毁,但是并不是刚开始就会直接创建coreSize的线程
- 阻塞队列是无限大的,不会执行拒绝策略。
- 可能会堆集无限的请求,导致OOM
newSingleThreadExecutor
- 相当于线程数为1的newFixedThreadPool,缺点和newFixedThreadPool一样
和一个线程的区别?
newSingleThreadExecutor | Thread |
---|---|
任务执行完成后,不会自动销毁,可以复用 | 任务执行完成后,会自动销毁 |
可以将任务存储在阻塞队列中,逐个执行 | 无法存储任务,只能执行一个任务 |
newScheduledThreadPool
- 可以创建定时的任务,以固定周期执行或者固定的延迟时间执行
- 如果任务时间超过了定时时长,就无法按照预定的时间执行
- 而Linux
crontab
定时处理器为了确保时间的正确性,会重新启一个线程
newWorkStealingPool
- 采用的ForkJoin框架,可以将任务进行分割,同时线程之间会互相帮助
- 阻塞队列采用的LinkedBlockingDeque,可以进行任务窃取
五、使用线程池的好处
- 线程重用:线程的创建和销毁的开销是巨大的,而通过线程池的重用大大减少了这些不必要的开销,当然既然少了这么多消费内存的开销,其线程执行速度也是突飞猛进的提升。
- 控制线程池的并发数:线程不是并发的越多,性能越高,反而在线程并发太多时,线程的切换会消耗系统大量的资源,可以通过的设置线程池最大并发线程数目,维持系统高性能
- 线程池可以对线程进行管理:虽然线程提供了线程组操控线程,但是线程池拥有更多管理线程的API
- 可以储存需要执行的任务:当任务提交过多时,可以将任务储存起来,等待线程处理