线程池原理分析(一)-线程池体系结构
概述
随着摩尔定律失效,多核计算器成为主流,多线程提高执行效率就变得异常重要,而线程的创建销毁又是一个开销比较大的操作,于是就产生了线程池,把使用过的线程放入线程池中,重复利用,其思想就是这些,很简单,但是线程池的管理就没有那么简单了,首先要管理好多个线程,然后还要管理任务,所以整个事情就变得复杂起来,本篇先介绍一下线程池的继承结构图,简单介绍下各个接口和类的作用,下一篇文章再分析线程池。
线程池继承结构图
简化结构图如下
上面第一幅图把线程池的关键的类都画出来了,第二幅图是一个简化的图,这个里面的实现才是核心,第一幅图先不要管,先介绍第二幅图,当第二幅图介绍完之后,再回过头来看第一幅图。
Executor
public interface Executor { void execute(Runnable command); }
线程池是用来管理线程的,而线程是用来执行任务的,所以在最顶层设计了Executor接口,该接口就一个方法,用来执行任务。
ExecutorService
public interface ExecutorService extends Executor { //关闭线程池,但阻塞队列中的任务继续执行完之后才关闭 void shutdown(); //关闭线程池,阻塞队列中的任务直接丢弃 List<Runnable> shutdownNow(); //是否处于SHUTDOWN状态 boolean isShutdown(); //是否处于TERMINATED状态 boolean isTerminated(); //等待线程池进入TERMINATED状态 boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException; //提交任务,不过任务有返回值 <T> Future<T> submit(Callable<T> task); //提交任务,不过任务有返回值 <T> Future<T> submit(Runnable task, T result); //提交Runnable任务,会自动封装成Callable任务 Future<?> submit(Runnable task); /** * 执行给定的任务,当所有任务完成时,返回保持任务状态和结果的 Future 列表 */ <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException; /** * 执行给定的任务,当所有任务完成或超时期满时(无论哪个首先发生),返回保持任务状态和结果的 Future 列表 */ <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException; /** * 执行给定的任务,如果某个任务已成功完成(也就是未抛出异常),则返回其结果 */ <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException; /** * 执行给定的任务,如果在给定的超时期满前某个任务已成功完成(也就是未抛出异常),则返回其结果 */ <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; }
这个接口主要作用就是提交任务,让接口Executor中的execute方法执行,开头的几个方法shutdow(),shutdownNow()等,这些都是让线程池结束的方法,但是线程池有好几个状态,而线程池结束就是在这些状态之间转换,具体到介绍ThreadPoolExecutor类的时候在介绍。
AbstractExecutorService
public abstract class AbstractExecutorService implements ExecutorService { //代码就不贴了 }
这个类是一个抽象类,实现了ExecutorService,主要是为该接口的方法提供一些默认的实现。
ThreadPoolExecutor
这个类就是线程池实现的核心类,线程池的线程管理和阻塞队列管理都是它完成的,下一篇文章会详细介绍这个类。
ScheduledExecutorService
public interface ScheduledExecutorService extends ExecutorService { //执行一次性任务,固定延迟之后开始执行,有返回结果 public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit); //一次性任务,有返回 public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit); //固定延迟之后开始执行,之后按照固定时间间隔执行,如果在间隔时间内上一次调度还没有执行完 //不会影响下一次执行 public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); //固定延迟之后开始执行,之后按照固定时间间隔执行,如果在间隔时间内上一次调度还没有执行完 //下一次调度任务即便时间已经到了也不会执行,会等到上一次任务执行完之后在等待固定时间间隔之后执行 public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit); }
在ExecutorService接口中定义了很多的提交任务的方法,但是那些提交任务的方法都没有设置延迟执行的,这个接口定义了4个固定延时的方法,其中最后两个还可以定时执行。
ScheduledThreadPoolExecutor
java.util.concurrent.ScheduledThreadPoolExecutor
,继承 ThreadPoolExecutor ,并且实现 ScheduledExecutorService 接口,是两者的集大成者,相当于提供了“延迟”和“周期执行”功能的 ThreadPoolExecutor 。
这里简单提一下其设计思想,大家都知道线程池中有一个队列用于放任务,如果这个队列是一个特殊的队列,比如DelayedQueue(ScheduledThreadPoolExecutor中使用的队列和这个类似),这个队列可以控制队列中的节点延迟时间,如果队列中的节点没有到期,通过take等方法就无法获取到值,会阻塞,直到延迟时间结束,take才可以获取到,这样就可以做到定时执行。到这里可能有的胖友有疑问,如果从队列中取出来,那队列中不就没有这个任务了,那下次再定时执行怎么办,这个就是ScheduledFutureTask(这个类上面没有介绍,看名字大家应该可以猜到,和FutureTask作用差不多)的事情了,该类会重写run方法,在run方法中会再次将任务放入到队列中。
具体可以参考:【死磕Java并发】—–J.U.C之线程池:ScheduledThreadPoolExecutor
小结
以上就是第二幅图相关接口和类的介绍,其中线程池和定时调度类没有详细介绍,只是大致提了一下,因为这两个类很复杂,后面专门写文章介绍。下面就看一下第一幅图多出来的部分的内容。
Executors
静态工厂类,提供了 Executor、ExecutorService 、ScheduledExecutorService、ThreadFactory 、Callable 等类的静态工厂方法,通过这些工厂方法我们可以得到相对应的对象。举例:
ExecutorService service = Executors.newFixedThreadPool(10);
Future
public interface Future<V> { //取消当前任务 boolean cancel(boolean mayInterruptIfRunning); //是否已经取消 boolean isCancelled(); //是否执行完成 boolean isDone(); //获取执行完成的结果,如果还没有执行完成就阻塞 V get() throws InterruptedException, ExecutionException; //获取执行完成的结果,等待固定的时间,如果规定时间内还没有执行完,抛异常 V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; }
获取任务执行状态的一个类,当任务提交到线程池,需要获取任务执行的结果,如果线程池中堆积了很多的任务,那很多任务都无法及时的获取执行结果,这个时候就看用户的个人选择了,是使用get等待,还是直接取消该任务。
RunnableFuture
public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); }|
该接口继承了Runnable,那实现这个接口的类就可以直接作为任务传入线程池执行,而该接口同时又实现了Future接口,这就说明还可以去获取任务的执行状态。
FutureTask
public class FutureTask<V> implements RunnableFuture<V> { private volatile int state; private static final int NEW = 0; private static final int COMPLETING = 1; private static final int NORMAL = 2; private static final int EXCEPTIONAL = 3; private static final int CANCELLED = 4; private static final int INTERRUPTING = 5; private static final int INTERRUPTED = 6; //省略部分代码 }
这个类就实现了RunnableFuture,该类是一个核心类,如果要获取线程的执行结果,就是通过这个类获取的,该类的实现和AQS很类似,里面也是搞了一个阻塞队列,然后搞了一个state来管理状态。其实这个阻塞队列就是一个单链表,里面放的是要获取该任务执行结果的线程,如果该任务还没有执行完,那这些线程就无法获取到结果,就会放入到了链表中,state的作用就是存放任务的状态,上面贴出的代码中可以看出任务可以处于以上7种状态,多个线程都可以操作state,所以要通过CAS来加锁修改,其实思想和AQS很类似。后面会详细分析。
具体大家可以参考:FutureTask源码解析(2)——深入理解FutureTask
CompletionService
public interface CompletionService<V> { //提交任务 Future<V> submit(Callable<V> task); //提交任务 Future<V> submit(Runnable task, V result); //获取任务执行结果 Future<V> take() throws InterruptedException; //获取任务执行结果 Future<V> poll(); //获取任务执行结果 Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException; }
这个接口主要作用其实就是为了弥补Future的不足,通过Future的get方法可以获取执行结果,这个确实没问题,思考一下如下场景,如果提交了很多的任务,然后就等待获取这些任务的执行结果,但是又不知道哪个任务先执行完,如果从头开始遍历,如果第一个任务一下子执行了2个小时,那就阻塞了2小时,但是在这个两小时中可能已经有很多别的任务执行完了,我们本可以先去处理那些已经执行完的任务,但是却白白等了2小时,当然了,对于有经验的胖友来说,可以遍历然后调用isDone()方法,没有完成就过,然后一直死循环,直到所有的线程执行完,这样子也行,只是不够优雅。
大家如果仔细看上面接口的定义,会发现很多的方法好像是操作队列的方法,其实像take,poll就是操作队列的方法,具体的实现是这样的,CompletionService的实现类会在类中定义一个阻塞队列,当有任务执行完了,任务的执行结果就放入队列中,然后我们只需要调用take方法获取就可以了,当队列中为空的时候,就阻塞等待,这样实现起来就很优雅。
具体大家可以参考:CompletionService和ExecutorCompletionService详解
ExecutorCompletionService
public class ExecutorCompletionService<V> implements CompletionService<V> { private final Executor executor; private final AbstractExecutorService aes; private final BlockingQueue<Future<V>> completionQueue; //省略部分代码 }
这个类就是上面那个接口的实现类,里面有如下代码
private final BlockingQueue<Future<V>> completionQueue;
这个阻塞队列就是放执行完成结果的。具体的思想在上面介绍接口的时候已经介绍了,这里就不在赘述了。
Delayed
public interface Delayed extends Comparable<Delayed> { //获取延迟时间 long getDelay(TimeUnit unit); }
该接口定义很简单,就只有一个方法。
ScheduledFuture
public interface ScheduledFuture<V> extends Delayed, Future<V> { }
这个接口从名称就可以看出来,就是实现延时执行,并且还要获取执行结果。
总结
上面笼统的把每个类和接口给简单介绍了一下,下面梳理一下。其实和线程池相关的类中有四个类是很核心的类,分别如下:
-
ThreadPoolExecutor:这个就不用多说了,实现线程池的,核心类
- FutureTask:如果不用获取定时任务执行结果,直接使用Runnable提交任务就可以了,如果要获取执行结果就需要使用FutureTask提交任务
-
ExecutorCompletionService:该类组合了上面两个类,线程池的作用是用来执行任务,而执行的任务就是FutureTask,然后该类还使用了一个阻塞队列把已经执行完成的任务放入到队列,方便获取
-
ScheduledThreadPoolExecutor:该类是实现定时调度的核心类,其执行也是需要依赖线程池
从上面的介绍中大家可以看出,最后两个类都要依赖于线程池,所以ThreadPoolExecutor才是最核心的,下一篇文章就介绍这个类。上面的介绍有点乱,只是想把线程池相关的重要的类都给梳理下,有个整体印象,之后在学习的时候知道自己处在什么位置,还有哪些没有学习到。
参考:
【死磕 Java 并发】—– J.U.C 之线程池:线程池的基础架构
【死磕Java并发】—–J.U.C之线程池:线程池的基础架构
jdk1.8源码