介绍
线程池就是对于Thread的复用,避免创建大量的工作线程导致资源的开销及浪费,甚至引发oom发生。
ThreadPoolExecutor
先来看一下线程池的构造函数各个参数意义
public ThreadPoolExecutor(
int corePoolSize, //核心线程数
int maximumPoolSize, //最大线程数 (核心+非核心线程数)
long keepAliveTime, //非核心线程数超时时间
TimeUnit unit, //超时时间单位
BlockingQueue<Runnable> workQueue, //保存线程的队列
ThreadFactory threadFactory, //创建线程的工程
RejectedExecutionHandler handler) //超载策略
-
corePoolSize
核心线程的数量,默认核心线程在限制状态下也会存辉在线程池中。如果设置了allowCoreThreadTimeOut(true), 核心线程会跟随超时策略终止。 -
maximumPoolSize
可容纳的最大线程数量,包含核心线程数+非核心线程数,到达这个数量之后,后续的新任务将会走超载的策略。 -
keepAliveTime
非核心线程闲置的超时时长, 超过这个时间, 非核心线程将会被回收。 -
unit
配合上面参数, 指定超时的时间单位。
天 TimeUnit.DAYS
小时 TimeUnit.HOURS
分钟 TimeUnit.MINUTES
毫秒 TimeUnit.MILLISECONDS
微秒 TimeUnit.MICROSECONDS
毫微秒 TimeUnit.NANOSECONDS -
workQueue
用来保存被阻塞的任务的队列
阻塞队列 | 说明 |
---|---|
SynchronousQueue | 它内部没有任何的缓存空间。只有当我们试着取走的时候才可能存在。 |
PriorityBlockingQueue | 带有优先级的无限阻塞队列。 |
ArrayBlockingQueue | 基于数组实现的有界的阻塞队列,原则对队列中的元素进行排序。 |
LinkedBlockingQueue | 基于链表实现的阻塞队列,原则对队列中的元素进行排序。 |
我们还能够通过实现BlockingQueue接口来自定义我们所需要的阻塞队列。
- threadFactory
线程工厂,为线程池提供新线程的创建。ThreadFactory是一个接口,里面只有一个newThread方法。 默认为DefaultThreadFactory类。
- handler
是RejectedExecutionHandler对象,而RejectedExecutionHandler是一个接口,里面只有一个rejectedExecution方法。当任务队列已满并且线程池中的活动线程已经达到所限定的最大值或者是无法成功执行任务,这时候ThreadPoolExecutor会调用RejectedExecutionHandler中的rejectedExecution方法。在ThreadPoolExecutor中有四个内部类实现了RejectedExecutionHandler接口。在线程池中它默认是AbortPolicy,在无法处理新任务时抛出RejectedExecutionException异常。
下面是在ThreadPoolExecutor中提供的四个可选值。
可选值 | 说明 |
---|---|
CallerRunsPolicy | 只用调用者所在线程来运行任务。 |
AbortPolicy | 直接抛出RejectedExecutionException异常。 |
DiscardPolicy | 丢弃掉该任务,不进行处理。 |
DiscardOldestPolicy | 丢弃队列里最近的一个任务,并执行当前任务。 |
我们也可以通过实现RejectedExecutionHandler接口来自定义我们自己的handler。如记录日志或持久化不能处理的任务。
ThreadPoolExecutor的使用
ExecutorService service = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
对于ThreadPoolExecutor有多个构造方法,对于上面的构造方法中的其他参数都采用默认值。可以通过execute和submit两种方式来向线程池提交一个任务。 execute 当我们使用execute来提交任务时,由于execute方法没有返回值,所以说我们也就无法判定任务是否被线程池执行成功。
service.execute(new Runnable() {
public void run() {
System.out.println("execute方式");
}
});
submit
当我们使用submit来提交任务时,它会返回一个future,我们就可以通过这个future来判断任务是否执行成功,还可以通过future的get方法来获取返回值。如果子线程任务没有完成,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时候有可能任务并没有执行完。
Future<Integer> future = service.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("submit方式");
return 2;
}
});
try {
Integer number = future.get();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
线程池关闭
调用线程池的shutdown()或shutdownNow()方法来关闭线程池
shutdown原理:将线程池状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
shutdownNow原理:将线程池的状态设置成STOP状态,然后中断所有任务(包括正在执行的)的线程,并返回等待执行任务的列表。
中断采用interrupt方法,所以无法响应中断的任务可能永远无法终止。 但调用上述的两个关闭之一,isShutdown()方法返回值为true,当所有任务都已关闭,表示线程池关闭完成,则isTerminated()方法返回值为true。当需要立刻中断所有的线程,不一定需要执行完任务,可直接调用shutdownNow()方法。
线程池执行流程
-
如果在线程池中的线程数量没有达到核心的线程数量,这时候就回启动一个核心线程来执行任务。
-
如果线程池中的线程数量已经超过核心线程数,这时候任务就会被插入到任务队列中排队等待执行。
-
由于任务队列已满,无法将任务插入到任务队列中。这个时候如果线程池中的线程数量没有达到线程池所设定的最大值,那么这时候就会立即启动一个非核心线程来执行任务。
-
如果线程池中的数量达到了所规定的最大值,那么就会拒绝执行此任务,这时候就会调用RejectedExecutionHandler中的rejectedExecution方法来通知调用者。
四种线程池类
Java中四种具有不同功能常见的线程池。他们都是直接或者间接配置ThreadPoolExecutor来实现他们各自的功能。这四种线程池分别是:
- newFixedThreadPool
- newCachedThreadPool
- newScheduledThreadPool
- newSingleThreadExecutor
这四个线程池可以通过Executors类获取
newFixedThreadPool
通过Executors中的newFixedThreadPool方法来创建,该线程池是一种线程数量固定的线程池。
ExecutorService service = Executors.newFixedThreadPool(4);
在这个线程池中 所容纳最大的线程数就是我们设置的核心线程数。 如果线程池的线程处于空闲状态的话,它们并不会被回收,除非是这个线程池被关闭。如果所有的线程都处于活动状态的话,新任务就会处于等待状态,直到有线程空闲出来。
由于newFixedThreadPool只有核心线程,并且这些线程都不会被回收,也就是 它能够更快速的响应外界请求 。从下面的newFixedThreadPool方法的实现可以看出,newFixedThreadPool只有核心线程,并且不存在超时机制,采用LinkedBlockingQueue,所以对于任务队列的大小也是没有限制的。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newCachedThreadPool
通过Executors中的newCachedThreadPool方法来创建。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
通过s上面的newCachedThreadPool方法在这里我们可以看出它的 核心线程数为0, 线程池的最大线程数Integer.MAX_VALUE。而Integer.MAX_VALUE是一个很大的数,也差不多可以说 这个线程池中的最大线程数可以任意大。
当线程池中的线程都处于活动状态的时候,线程池就会创建一个新的线程来处理任务。该线程池中的线程超时时长为60秒,所以当线程处于闲置状态超过60秒的时候便会被回收。 这也就意味着若是整个线程池的线程都处于闲置状态超过60秒以后,在newCachedThreadPool线程池中是不存在任何线程的,所以这时候它几乎不占用任何的系统资源。
对于newCachedThreadPool他的任务队列采用的是SynchronousQueue,上面说到在SynchronousQueue内部没有任何容量的阻塞队列。SynchronousQueue内部相当于一个空集合,我们无法将一个任务插入到SynchronousQueue中。所以说在线程池中如果现有线程无法接收任务,将会创建新的线程来执行任务。
newScheduledThreadPool
通过Executors中的newScheduledThreadPool方法来创建。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
它的核心线程数是固定的,对于非核心线程几乎可以说是没有限制的,并且当非核心线程处于限制状态的时候就会立即被回收。
创建一个可定时执行或周期执行任务的线程池:
ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
service.schedule(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName()+"延迟三秒执行");
}
}, 3, TimeUnit.SECONDS);
service.scheduleAtFixedRate(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName()+"延迟三秒后每隔2秒执行");
}
}, 3, 2, TimeUnit.SECONDS);
输出结果:
pool-1-thread-2延迟三秒后每隔2秒执行
pool-1-thread-1延迟三秒执行
pool-1-thread-1延迟三秒后每隔2秒执行
pool-1-thread-2延迟三秒后每隔2秒执行
pool-1-thread-2延迟三秒后每隔2秒执行
schedule(Runnable command, long delay, TimeUnit unit): 延迟一定时间后执行Runnable任务;
schedule(Callable callable, long delay, TimeUnit unit): 延迟一定时间后执行Callable任务;
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit): 延迟一定时间后,以间隔period时间的频率周期性地执行任务;
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit): 与scheduleAtFixedRate()方法很类似,但是不同的是scheduleWithFixedDelay()方法的周期时间间隔是以上一个任务执行结束到下一个任务开始执行的间隔,而scheduleAtFixedRate()方法的周期时间间隔是以上一个任务开始执行到下一个任务开始执行的间隔,也就是这一些任务系列的触发时间都是可预知的。
ScheduledExecutorService功能强大,对于定时执行的任务,建议多采用该方法。
newSingleThreadExecutor
通过Executors中的newSingleThreadExecutor方法来创建,在这个线程池中只有一个核心线程,对于任务队列没有大小限制,也就意味着这一个任务处于活动状态时,其他任务都会在任务队列中排队等候依次执行。
newSingleThreadExecutor将所有的外界任务统一到一个线程中支持,所以在这个任务执行之间我们不需要处理线程同步的问题。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
线程池的使用技巧
需要针对具体情况而具体处理,不同的任务类别应采用不同规模的线程池,任务类别可划分为CPU密集型任务、IO密集型任务和混合型任务。(N代表CPU个数)
任务类别 | 说明 |
---|---|
CPU密集型任务 | 线程池中线程个数应尽量少,如配置N+1个线程的线程池。 |
IO密集型任务 | 由于IO操作速度远低于CPU速度,那么在运行这类任务时,CPU绝大多数时间处于空闲状态,那么线程池可以配置尽量多些的线程,以提高CPU利用率,如2*N。 |
混合型任务 | 可以拆分为CPU密集型任务和IO密集型任务,当这两类任务执行时间相差无几时,通过拆分再执行的吞吐率高于串行执行的吞吐率,但若这两类任务执行时间有数据级的差距,那么没有拆分的意义。 |