快速到达看这里-->
线程池的好处
- 加快响应速度
- 合理利用CPU和内存
- 统一管理资源
线程池适用场合
- 服务器接收大量请求时,使用线程池可以大大减少线程的创建和销毁的次数,提高服务器的工作效率
- 开发中,如果需要创建5个以上的线程。那么就可以使用线程池来管理
线程池的创建
线程池的构造函数的参数
new ThreadPoolExecutor
corePoolSize
指核心线程数:线程池在初始化完毕后,默认情况下,线程池没有任何线程,线程池会等到任务来的时候创建新线程去执行任务- 线程池可能会在核心线程数的基础上增加一些线程,但是育一个上限,就是
maximumPoolSize
- 如果线程池当前线程数多余corePoolSize,多余的线程空闲时间超过
keepAliveTime
,这部分线程就会被终止 - 新的线程是由
ThreadFactory
创建的,通常使用默认的就够用了- 默认的使用Executor.defaultThreadFactory(),创建出来的线程都在同一个线程组,拥有同样的优先级,且都不是守护线程
- 自己指定ThreadFactory,就可以改变线程名,线程组,优先级,是否守护线程等
- 常用的
workQueue
- 直接交换:SynchronousQueue 本身内部没有容量
- 无界队列:LinkedBlockingQueue 队列放不满
- 有界队列:ArrayBlockingQueue 可以自己设置大小,maximumPoolSize才有意义
添加线程规则
- 如果线程数小于corePoolSize,即使其他工作线程处于空闲状态,也会创建一个新线程来运行任务
- 如果线程数等于或大于corePoolSize但少于maximumPoolSize,将任务放入队列
- 如果队列满了,并且线程数小于maximumPoolSize,则创建一个新线程来运行任务
- 如果队列已满,并且线程数大于或等于maximumPoolSize,则拒绝该任务
举个例子:
- 线程池:核心池大小5,最大池大小10,队列100
- 前5个请求会在核心池创建5个线程,然后进来的任务会添加到队列中,直到达到100。当队列满了后,再创建新线程,当总线程数达到maximumPoolSize = 10 后,再来任务请求,就直接拒绝
增减线程的特点
- 通过设置maximumPoolSize 和 corePoolSize相同,就可以创建固定大小的线程池。
- 线程池希望保持较少的线程数,并且只有在负载变得很大时才增加它
- 通过设置maximumPoolSize 为非常大的值,可以允许线程池容纳任意数量的并发任务
- 只有队列填满时才创建多余corePoolSize的线程,所以使用的是无边界队列(如LinkedBlockingQueue),那么线程数就不会超过corePoolSize
线程池应该手动创建还是自动创建
-
手动创建更好,可以更明确线程池的运行规则,避免资源浪费
-
正确创建线程池的方法
- 根据不同的业务场景,自己设置线程池参数
- 线程池中线程数目设定
- CPU密集型(加密,计算hash等):最佳线程池为CPU核心数的1-2倍左右
- 耗时IO型(读写数据库,文件,网络读写等):最佳线程数一般会大于CPU核心数很多倍
线程数 = CPU核心数(1+平均等待时间/平均工作时间)
执行线程池任务的两种方式
execute() 和 submit() 都是用来执行线程池任务的,它们最主要的区别是,submit() 方法可以接收线程池执行的返回值,而 execute() 不能接收返回值。
public static void main(String[] args) throws InterruptedException, ExecutionException {
ThreadPoolExecutor executor = new ThreadPoolExecutor
(2, 10, 10L, TimeUnit.SECONDS, new LinkedBlockingDeque<>(20));
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("execute method");
}
});
Future<String> submit = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("submit method");
return "success";
}
});
Thread.sleep(1000);
System.out.println(submit.get());
}
运行结果为:
execute method
submit method
success
几种常见线程池
- 几种常见自动创建的线程池
- newFixedThreadPool
任务队列采用的是LinkedBlockingQueue,没有容量上限 ,请求越来多导致无法及时处理时就会造成请求堆积,堆积足够多可能造成OOM - newSingleThreadExecutor
任务队列采用的是LinkedBlockingQueue,线程数为1,同样可能造成OOM - newCachedThreadPool
将maximumPoolSize 被设置成为Integer.MAX_VALUE,虽然能做到线程超时回收,但是当创建非常多的时候可能造成OOM - ScheduledThreadPool
按一定时间间隔周期性的运行线程,不使用大多数场景
- newFixedThreadPool
如何正确停止一个线程池
- shutdown()
调用shutdown()方法,进入shutdown状态,线程池开始停止,新的任务请求全部被拒绝,等待已经在运行的任务和任务队列中的任务全部执行完成后彻底停止 - isShutdown()
调用isShutdown()方法可以判断线程池是否开始停止
//预期为false
System.out.println("isShutdown1:"+executorService.isShutdown());
executorService.shutdown();
//预期为true
System.out.println("isShutdown2:"+executorService.isShutdown());
//shutdow之后的请求就会被拒绝
executorService.execute(new ShutDownTask());
- isTerminated()
调用 isTerminated()方法可以判断线程池是否彻底停止
executorService.shutdown();
//预期为false
System.out.println("isTerminated1:"+executorService.isTerminated());
Thread.sleep(10000);
//等待十秒,再看是否彻底停止,预期为true
System.out.println("isTerminated2:"+executorService.isTerminated());
- awaitTerminated()
检测一段时间内线程池是否彻底停止
boolean b = executorService.awaitTermination(3L, TimeUnit.SECONDS);//3秒内是否彻底停止
System.out.println(b);
- shutdownNow()
对正在运行的线程调用interrupt通知中断,线程池队列中等待的任务会作为返回值返回,方便将未执行的任务加入到新的线程池或者写入日志
List<Runnable> runnables = executorService.shutdownNow();
线程池停止的完整代码如下:
/**
* 〈关闭线程池〉
*
* @author Chkl
* @create 2020/3/11
* @since 1.0.0
*/
public class ShutDown {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService =
Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
executorService.execute(new ShutDownTask());
}
Thread.sleep(1500);
List<Runnable> runnables = executorService.shutdownNow();
// //预期为false
// System.out.println("isShutdown1:"+executorService.isShutdown());
// executorService.shutdown();
// //预期为true
// System.out.println("isShutdown2:"+executorService.isShutdown());
// //shutdow之后的请求就会被拒绝
// //executorService.execute(new ShutDownTask());
//
// //预期为false
// System.out.println("isTerminated1:"+executorService.isTerminated());
// Thread.sleep(10000);
// //等待十秒,再看是否彻底停止,预期为true
// System.out.println("isTerminated2:"+executorService.isTerminated());
}
static class ShutDownTask implements Runnable {
@Override
public void run() {
try {
Thread.sleep(500);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"被中断了");
}
}
}
}
任务太多,怎么拒绝
拒绝时机
- 当Executor关闭时,提交新任务会被拒绝
- 当Executor的最大线程和最大容量使用有限边界并且已经饱和时
拒绝策略
- AbortPolicy
终止策略,线程池会抛出异常并终止执行,它是默认的拒绝策略; - DiscardPolicy
偷偷地丢弃任务,不通知 - DiscaedOldestPolicy
偷偷丢弃最老的(队列中存在时间最久的)一个,不通知 - CallerRunsPolicy
让提交任务的线程去帮忙执行
如何设计一个简单的线程池
- 首先需要一个线程池的接口,提供提交任务的接口,任务执行完成后异步返回结果
- 线程池的实现需要一个任务队列和一个线程存储池,任务队列可以使用阻塞队列来实现
- 当有任务加入时首先进入任务队列,任务被执行时从线程池子中获取线程进行执行,执行完成后将线程放回池子,将结果异步返回
- 需要考虑几个问题:
- 存放线程池子大小设置多少
- 存放任务的队列大小设置多少
- 任务队列满了之后对外应该有什么操作
本文参考了:《玩转Java并发工具》
更多Java面试复习笔记和总结可访问我的面试复习专栏《Java面试复习笔记》,或者访问我另一篇博客《Java面试核心知识点汇总》查看目录和直达链接