借助于很多强大的框架,现在我们已经很少直接去管理线程,框架的内部都会为我们自动维护一个线程池。例如我们使用最多的okHttp以及他的封装框架Retrofit,线程封装框架RxJava和kotlin协程等等。为了更好地使用这些框架,则必须了解他的实现原理,而了解他的原理,线程池是永远绕不开的话题。
线程的创建与切换的成本是比较昂贵的。JVM的线程实现使用的是轻量级进程,也就是一个线程对应一个cpu核心。因此在创建与切换线程时,则会涉及到系统调用,是开销比较大的过程。为了解决这个问题,线程池诞生了。
与很多连接池,如sql连接池、http连接池的思想类似,线程池的出现是为了复用线程,减少创建和切换线程所带来的开销,同时可以更方便地管理线程。线程池的内部维护有一定数量的线程,这些线程就像一个个的“工人”,我们只需要向线程池提交任务,那么这些任务就会被自动分配到这些“工人”,也就是线程去执行。
线程池的好处:
减少资源损耗。重用线程、控制线程数量,减少线程创建和切换所带来的开销。
提高响应速度。可直接使用线程池中空闲的线程而不必等待线程的创建。
方便管理线程。线程池可以对其中的线程进行简单的管理,如实时监控数据调整参数、设置定时任务等。
这个系列文章主要分两篇:使用篇与原理篇。作为使用篇,顾名思义,主要是了解什么是线程池以及如何正确去使用它。
线程池的主要实现类有两个:ThreadPoolExecutor和ScheduledThreadPoolExecutor,后者继承了前者。线程池的配置参数比较多,系统也预设了一些常用的线程池,放在Executors中,开发者只需要配置简单的参数就可以。线程池的可执行任务有两种对象:Runnable和Callable,这两种对象可以直接被线程池执行,以及他们的异步返回对象Future。
那么以上讲到的,就是本文要讨论的内容。首先,从线程池的可执行任务开始。
任务类型 Runnable public interface Runnable { public abstract void run(); }Runnable我们是比较熟悉的了。他是一个接口,内部只有一个方法 run ,没有参数,没有返回值。当我们提交一个Runnable对象给线程池执行时,他的 run 方法会被执行。
Callable public interface Callable<V> { V call() throws Exception; }与Runnable很类似,最大的不同就是他拥有返回值,而且会抛出异常。任务对象适合用于需要等待一个返回值的后台计算任务。
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; }Futrue他并不是一个任务,而是一个计算结果异步返回的管理对象。前面的Callable任务提交给线程池之后,他需要一定的计算时间才能将结果返回,所以线程池会返回一个Future对象。我们可以调用他的 isDone 方法来检测是否完成,如果完成可以调用 get 方法来获取结果。同时也可以通过 cancel 和 isCancel 来取消或者判断任务是否被取消。如下图:
当Future未被执行或正在执行中时,get方法会阻塞直到执行完成。如果不希望等待时间过长,可以调用它另外一个带有时间参数的方法,该方法等待指定时间之后,如果任务尚未完成则会抛出TimeoutException异常。
当Future执行完成之后,get方法会返回结果;而此时如果任务是被取消,那么会抛出异常。
当一个任务尚未被执行时,cancel方法会让该任务不会被执行,直接结束。
当任务正在执行,cancel方法的参数如果为false,那么不会中断任务;而如果参数是true则会尝试中断任务。
当任务已完成,取消方法返回false,取消失败。
Futrue接口的具体实现类是FutureTask,该类同时实现了Runnable接口,可以被线程池直接执行。我们可以通过传入一个Callable对象来创建一个FutureTask。如果是Runnable对象,则可以通过 Executors.callable() 来构造一个Callable对象,只不过这个Callable对象返回null,所构造出来的Future对象get方法在成功时也会返回null。当FutureTask的 run 方法被执行后,其所包含的任务开始执行。
当然,我们也可以单独使用FutureTask,如下:
// 创建一个FutureTask FutureTask<String> future = new FutureTask<>(() -> { Thread.sleep(1000); return "一只修仙的猿"; }); // 使用一个后台线程来执行他的run方法 new Thread(future).start(); // 执行结束之后可以通过他的get方法得到返回值 System.out.println(future.get());当然,如果我们要这样使用的话,那为什么不用线程池呢?(手动狗头)
接下来介绍线程池的两个主要类:ThreadPoolExecutor和ScheduledThreadPoolExecutor。
ThreadPoolExecutor 概述