java并发编程实战:第六章----任务执行
任务:通常是一些抽象的且离散的工作单元。大多数并发应用程序都是围绕"任务执行"来构造的,把程序的工作分给多个任务,可以简化程序的组织结构便于维护
一、在线程中执行任务
任务的独立性:任务并不依赖于其他任务的状态,结果和边缘效应。独立的任务可以实现并行执行
1、串行的执行任务
所有的任务放在单个线程中串行执行,程序简单,安全性高,不涉及同步等情况,缺点也显而易见,无法提高吞吐量和响应速度,适合任务数量很少并且执行时间很长时,或者只为单个用户使用,并且该用户每次只发出一个请求。
2、显示的创建线程
为每一个请求创建一个线程,将任务的处理从主线程中分离出来,多个任务可以并行处理,充分利用了系统资源,提高吞吐量和相应速度,要求处理代码必须是线程安全的
3、无限创建线程的不足
线程生命周期的开销非常高;太多线程会消耗系统资源,空闲线程的内存空间占用,大量线程竞争CPU时产生其他性能开销;稳定性:破坏这些限制底层操作系统对线程的限制很可能抛出OutOfMemoryError异常
总结:在一定范围内,增加线程有助于提高吞吐量,但是再多就可能导致性能下降。
二、Executor框架
任务是一组逻辑单元,而线程是使任务异步执行的机制
- Executor简化了线程的管理工作,并且java.util.concurrent提供了一种灵活的线程池作为Executor框架的一部分。
- Executor基于生产者—消费者设计模式,提交任务的操作单元相当于生产者(生成待完成的工作单元),执行任务的线程相当于消费者(执行完这些工作单元)
- 将提交过程和执行过程解耦,用Runnable表示执行任务
1、基于Executor的web服务器
1 public class ThreadPerTaskWebServer { 2 private static final int NTHREADS = 100; 3 /** 4 * 创建固定线程数量的线程池 5 */ 6 private static final Executor exec = 7 Executors.newFixedThreadPool(NTHREADS); 8 public static void main(String[] args) throws IOException { 9 ServerSocket server = new ServerSocket(80); 10 boolean listening = true; 11 while (listening){ 12 final Socket connection = server.accept(); //阻塞等待客户端连接请求 13 Runnable task = new Runnable() { 14 @Override 15 public void run() { 16 handlerRequest(connection); 17 } 18 }; 19 exec.execute(task); 20 } 21 server.close(); 22 } 23 ... 24 }
Executor创建了含有100个线程的线程池来处理任务
若想更改任务的处理方式,只需要使用不用的Executor实现
2、执行策略
- 根据可用的资源和对服务质量的要求制定合理的执行策略
- 将任务的提交与任务的执行解耦,有助于在部署阶段选择与硬件最匹配的执行策略
3、线程池:管理一组同构工作线程的资源池。
线程池vs工作队列:工作者线程来自线程池,从工作队列获取任务,执行完毕回到线程池
优点:不仅可以在处理多个请求时分摊在线程创建和销毁过程中产生的巨大开销,另外一个好处就是当请求到达时,工作线程通常已经存在,因此不会由于等待线程创建而延迟任务的执行
- newFixedThreadPool:固定长度的线程池,即线程池的规模有上限。
- newCachedThreadPool:可缓存的线程池,如果线程池的当前规模超过了处理需求时,将回收空闲的线程,而当需求增加时,则可以添加新的线程,注意线程池的规模不存在任何限制。
- newSingleThreadExecutor:单线程的Executor,通过创建单个工作者线程来串行的执行任务,如果此线程异常结束,Executor会创建另一个线程来代替。注意此模式能确保依照任务在队列中的顺序来串行执行(例如FIFO、LIFO、优先级)。
- newScheduledThreadPool:创建固定长度的线程池,而且以延迟或者定时的方式来执行任务。
4、Executor的生命周期
newXXXThreadPool都是返回的ExecutorService
ExecutorService的生命周期主要有三种状态:运行、关闭和已终止。
为了解决执行服务的生命周期问题,ExecutorService扩展了Executor接口,添加了管理生命周期的方法。
- shutdown:关闭线程池,不再接受新任务,等待已经提交的任务完成
- shutdownNow:强制立即关闭线程池,返回的等待执行的任务列表,执行中的任务抛出中断异常
- isShutdown:是否处于正在关闭状态
- isTerminated:是否结束
- awaitTerminated:阻塞等待关闭完成
5、延迟任务和周期任务
通过ScheduledThreadPoolExecutor来代替Timer,TimerTask。
- Timer基于绝对时间,ScheduledThreadPoolExecutor基于相对时间。
- Timer执行所有定时任务只能创建一个线程,若某个任务执行时间过长,容易破坏其他TimerTask的定时精确性。
- Timer不捕获异常,Timetask抛出未检查的异常会终止定时器线程,已经调度但未执行的TimerTask将不会再执行,新的任务也不会被调度,出现"线程泄漏"
1 public class OutOfTime { 2 public static void main(String[] args) throws InterruptedException { 3 Timer timer = new Timer(); 4 timer.schedule(new ThrowTask(), 1); //第一个任务抛出异常 5 Thread.sleep(1000); 6 timer.schedule(new ThrowTask(), 1); //第二个任务将不能再执行, 并抛出异常Timer already cancelled. 7 Thread.sleep(5000); 8 System.out.println("end."); 9 } 10 11 static class ThrowTask extends TimerTask{ 12 13 @Override 14 public void run() { 15 throw new RuntimeException("test timer's error behaviour"); 16 } 17 } 18 }
三、找出可利用的并行性
1、携带结果的任务Callable与Future
Runnable的缺陷:不能返回一个值,或抛出一个异常
Callable和Runnable都描述抽象的计算任务,Callable可以返回一个值,并可以抛出一个异常
Executor执行任务的4个生命周期:创建,提交,开始,完成。Executor框架中,可以取消已提交但未开始执行的任务,对于已经开始执行的任务,只能当他们能响应中断时,才能取消,取消已经完成的任务不会有影响。
Future表示了一个任务的生命周期,提供了相应的方法判断是否完成或被取消以及获取执行结果
- get方法:若任务完成,返回结果或抛出ExecutionException;若任务取消,抛出CancellationException;若任务没完成,阻塞等待结果
- ExecutorService的submit方法提交一个Callable任务,并返回一个Future来判断执行状态并获取执行结果
- 安全发布过程:将任务从提交线程穿个执行线程,结果从计算线程到调用get方法的线程
2、异构任务并行化中存在的局限:当异构任务之间的执行效率悬殊很大时,对于整体的性能提升来看并不是很有效。
3、完成服务CompletionService(Executor+BlockingQueue)
使用BlockingQueue保存计算结果(Future),使用take和poll获取,计算部分同样委托给Executor
4、为任务设定时限:如果超出期望执行时间,将不要其结果
小结:通过围绕任务执行来设计应用程序,可以简化开发过程,并有助于实现并发。Executor框架将任务提交与执行策略解耦开来,同时还支持多种不同类型的执行策略。当需要创建线程来执行任务时,可以考虑使用Executor。要想在将应用程序分解为不同的任务时获得最大的好处,必须定义清晰的任务边界。某些应用程序中存在着比较明显的任务边界,而在其他一些程序中则需要进一步分析才能揭示出粒度更细的并行性。
方法小结
Future的get、cancel、isCancelled、isDone方法
get:在任务完成前一直阻塞。会抛出三种异常:CancellationException - 如果计算被取消、ExecutionException - 如果计算抛出异常、InterruptedException - 如果当前的线程在等待时被中断。
get(long timeout, TimeUnit unit):在超时之前且任务未完成则一直阻塞。除抛出以上三种异常
cancel(boolean mayInterruptIfRunning):试图取消对此任务的执行。如果任务已完成、或已取消,或者由于某些其他原因而无法取消,则此尝试将失败。当调用cancel时,如果调用成功,而此任务尚未启动,则此任务将永不运行。如果任务已经启动,则mayInterruptIfRunning参数决定了是否调用运行任务的线程的interrupt操作。
isCancelled:如果在任务正常完成前将其取消,则返回true
isDone:正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true
ExecutorService的submit、invokeAll、invokeAny方法
ExecutorService的有三个重载的submit方法:
1、 可以接收Runnable或Callable类型的任务,返回Future<?>类型的Future的get返回null。
2、 这三个方法都将提交的任务转换成了Future的实现类FutureTask实例,并作为submit的返回实例。
3、 另外调用这三个方法不会阻塞,不像invokeAll那样要等到所有任务完成后才返回,与不像invokeAny那样要等到有一个任务完成后才返回Future。
4、 这个三方法会调用Executor的execute来完成,因为Executor的execute会抛出RejectedExecutionException - 如果不能接受执行此任务、NullPointerException - 如果命令为 null这两个运行进异常,所以这三个方法也会抛出这两个异常。
T invokeAny(Collection<Callable<T>> tasks):
1、 只要某个任务已成功完成(也就是未抛出异常,这与任务完成概念不一样:任务完成是指定Future的isDone返回true,有可能是抛出异常后进行完成状态),才返回这个结果。一旦正常或异常返回后,则取消尚未完成的任务(即任务所运行的线程处理中断状态,一旦在它上面出现可中断阻塞的方法调用,则会抛出中断异常)。
2、 此方法会阻塞到有一个任务完成为止(正常完成或异常退出)。
3、 也是调用Executor的execute来完成
4、 调用get不会阻塞
invokeAny(Collection<Callable<T>> tasks, long timeout, TimeUnit unit):
1、 只要在给定的超时期满前某个任务已成功完成(也就是invokeAny方法不能抛出异常,包括Future.get所抛的异常),则返回其结果。一旦正常或异常返回后,则取消尚未完成的任务。
2、 此方法会阻塞到有一个任务完成为止(正常完成或异常退出)。
3、 也是调用Executor的execute来完成
4、 调用get不会阻塞
List<Future<T>> invokeAll(Collection<Callable<T>> tasks):
1、 只有当所有任务完成时,才返回保持任务状态和结果的 Future 列表。返回列表的所有元素的 Future.isDone() 为 true。注意,可以正常地或通过抛出异常来已完成任务。
2、 此方法会阻塞到所有任务完成为止(正常完成或异常退出)。
3、 也是调用Executor的execute来完成,如果任务执行过程中抛出了其他异常,则方法会异常退出,且取消所有其他还未执行完成的任务。
4、 返回的列表中的Future都是已经完成的任务,get时不会再阻塞
invokeAll(Collection<Callable<T>> tasks, long timeout, TimeUnit unit):
1、 当所有任务完成或超时期满时(无论哪个首先发生),返回保持任务状态和结果的 Future 列表(如果是超时返回的列表,则列表中的会包括这些还未执行完的任务,使用get获取结果时可能会抛出CancellationException异常)。返回列表的所有元素的 Future.isDone() 为 true。一旦返回后,即取消尚未完成的任务。注意,可以正常地或通过抛出异常来完成任务。
2、 此方法会阻塞到所有任务完成为止(正常完成或异常退出或超时)。
3、 也是调用Executor的execute来完成,如果任务执行过程中抛出了其他异常,则方法会异常退出,且取消所有其他还未执行完成的任务。
4、 返回的列表中的Future中会有因超时执行任务时异常而未执行完的任务,get时会抛出CancellationException或ExecutionException,当然所有的Future的get也不会阻塞。