【多线程】线程池
线程池的优势
- 管理线程
复用线程、控制最大并发数。对线程进行统一分配、调优和监控。 - 降低资源消耗
通过重复利用已创建的线程,降低线程创建、销毁的资源开销。 - 提高响应速度
任务不需要创建线程即可使用现有线程立刻执行。 - 实现任务线程队列的 缓存策略和拒绝机制
- 控制线程执行时间
如定时任务、周期执行 - 隔离线程环境
如果一个JVM中有两个服务,服务A对性能要求高,服务B相对较低,为了避免B服务消耗资源过多影响A服务效率,可以将两个服务的线程在不同的线程池,控制资源分配。
创建线程池
构造方法
ThreadPoolExecutor的构造方法如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- corePoolSize: 线程池核心线程数最大值。
- maximumPoolSize: 线程池最大线程数大小。corePoolSize和maximumPoolSize设置不当会影响效率,甚至耗尽线程资源。
- keepAliveTime: 线程池中非核心线程空闲的存活时间大小
- unit: 线程空闲存活时间单位
- workQueue: 存放任务的阻塞队列。workQueue设置不当会导致OOM。
- threadFactory: 用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。
- handler: 线城池的饱和策略事件,主要有四种类型。handler设置不当会导致提交任务时抛出异常。
线程池中线程工作顺序:
如果正在执行总线程数小于corePoolSize,Executor 会优先创建新线程到线程池;
如果正在执行总线程数大于corePoolSize,Executor 通常会将线程请求加入workQueue;
如果线程请求不能被加入workQueue,Executor 会创建新线程,除非当前正在运行的线程数大于maximumPoolSize,这种情况下Executor会根据拒绝策略拒绝任务。
corePoolSize -> 任务队列 -> maximumPoolSize -> 拒绝策略
便捷创建方法
Executors提供了创建线程池的便捷方法,如:Executors.newFixedThreadPool(int nThreads),但此方法隐藏了线程池的复杂性,会埋下隐患(定义一个Integer.MAX_VALUE大小的LinkedBlockingQueue,导致OOM; 无上限地创建线程,导致线程耗尽)
创建线程池的便捷方法 | 功能 | 应用场景 | 注意事项 |
---|---|---|---|
Executors.newFixedThreadPool(10) | 创建固定大小的线程池,可控制最大并发数 | 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。 | 线程池大小最好依赖系统资源定义,如Runtime.getRuntime().availableProcessors() |
Executors.newSingleThreadExecutor() | 创建一个线程的线程池(corePoolSize和maximumPoolSize都为1) | 适用于串行执行任务的场景,一个任务一个任务地执行。 | |
Executors.newCachedThreadPool() | 创建可缓存线程池(corePoolSize=0,keepAliveTime=60s说明线程资源释放后可以缓存一分钟) | 用于并发执行大量短期的小任务 | 线程数无上限,如果任务处理速度 < 任务提交速度,就会不断创建新线程导致资源耗尽。所以该线程池适合执行时间较短的任务 |
Executors.newScheduledThreadPool(1) | 创建一个定制执行线程的线程池 | 周期性执行任务的场景,需要限制线程数量的场景 |
提交任务到线程池的方式
提交方式 | 释放关心返回结果 |
---|---|
Future<T> submit(Callable<T> task) | 是 |
void execute(Runnable command) | 否 |
Future<T> submit(Runnable command) | 否。会返回Future,不过get()得到的永远是null; |
Runnable和Callable的区别
1、方法签名不同。void Runnable.run(), V callable.call() throw Exception
2、Runnable 无返回值,Callable有返回值
3、Runnable不可抛出异常,Callable可以抛出异常
使用线程池的注意点
- 避免使用无界队列
Executors提供的快捷方式默认使用的workQueue都是无界队列或者容量很大的队列,为避免OOM和线程资源耗尽,我们应该手动使用ThreadPoolExecutor的构造方法定义线程池。
ExecutorService executorService = new ThreadPoolExecutor(2, 2,
0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(512), // 使用有界队列,避免OOM
new ThreadPoolExecutor.DiscardPolicy());
- 明确线程池任务拒绝策略
拒绝策略 | 拒绝行为 |
---|---|
AbortPolicy(默认) | 抛出RejectedExecutionException异常 |
CallerRunsPolicy | 直接由任务提交线程执行该任务 |
DiscardOldestPolicy | 丢弃workQueue中最老的任务,尝试将新任务加入到workQueue中 |
DiscardPolicy | 什么都不做,直接丢弃新任务 |
面试题
面试问题1:Java的线程池说一下,各个参数的作用,如何进行的?
答:如上文所述。
面试问题2:使用线程池如何处理异常?
答:1、 线程内try-catch捕获处理;
2、submit Callable线程,通过Future.get()获取线程异常并处理。
3、重写ThreadPoolExecutor的afterExecute()方法,处理传递的异常引用。
class ExtendedExecutor extends ThreadPoolExecutor {
// 这可是jdk文档里面给的例子。。
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if (t == null && r instanceof Future<?>) {
try {
Object result = ((Future<?>) r).get();
} catch (CancellationException ce) {
t = ce;
} catch (ExecutionException ee) {
t = ee.getCause();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // ignore/reset
}
}
if (t != null)
System.out.println(t);
}
}}
4、为工作线程设置UncaughtExceptionHandler,在uncaughtException中处理异常。
ExecutorService threadPool = Executors.newFixedThreadPool(1, r -> {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler(
(t1, e) -> {
System.out.println(t1.getName() + "线程抛出的异常"+e);
});
return t;
});
threadPool.execute(()->{
Object object = null;
System.out.print("result## " + object.toString());
});
面试问题3:线程池都有哪几种工作队列?
答:
- ArrayBlockingQueue
有界队列。用数组实现的阻塞队列,按FIFO排序任务。 - LinkedBlockingQueue
可设置容量队列。基于链表实现的阻塞队列,按FIFO排序任务,可选择性设置容量大小,默认为最大长度Integer.MAX_VALUE。newFixedThreadPool、newSingleThreadExecutor等线程池使用了它。 - DelayedWorkQueue
延时队列。定时周期性地执行任务根据指定的执行时间顺序执行,否自按FIFO顺序执行。 - PriorityBlockingQueue
优先级队列。是具有优先级的无界阻塞队列。 - SynchronousQueue
同步队列。一个不存储元素的阻塞队列,每一个插入操作必须等到另一个线程调用移除操作,否则插入一直处于阻塞状态。newCachedThreadPool使用了该队列。
面试问题4:说说几种常见的线程池及使用场景?
答:见上文。
面试问题5:线程池有哪些状态?
答:
RUNNING
- 该状态的线程池会接收新任务,并处理阻塞队列中的任务;
- 调用线程池的shutdown()方法,可以切换到SHUTDOWN状态;
- 调用线程池的shutdownNow()方法,可以切换到STOP状态;
SHUTDOWN
- 该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
- 队列为空,并且线程池中执行的任务也为空,进入TIDYING状态;
STOP
- 该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
- 线程池中执行的任务为空,进入TIDYING状态;
TIDYING
- 该状态表明所有的任务已经运行终止,记录的任务数量为0。
- terminated()执行完毕,进入TERMINATED状态
TERMINATED
- 该状态表示线程池彻底终止
面试问题6:讲讲newScheduledThreadPool工作机制
答:
- 添加一个任务
- 线程池中的线程从 DelayQueue 中取任务
- 线程从 DelayQueue 中获取 time 大于等于当前时间的task
- 执行完后修改这个 task 的 time 为下次被执行的时间
- 这个 task 放回DelayQueue队列中
/**
创建一个给定初始延迟的间隔性的任务,之后的下次执行时间是上一次任务从执行到结束所需要的时间+* 给定的间隔时间
*/
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleWithFixedDelay(()->{
System.out.println("current Time" + System.currentTimeMillis());
System.out.println(Thread.currentThread().getName()+"正在执行");
}, 1, 3, TimeUnit.SECONDS);
/**
创建一个给定初始延迟的间隔性的任务,之后的每次任务执行时间为 初始延迟 + N * delay(间隔)
*/
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleAtFixedRate(()->{
System.out.println("current Time" + System.currentTimeMillis());
System.out.println(Thread.currentThread().getName()+"正在执行");
}, 1, 3, TimeUnit.SECONDS);;