并发(4) 任务执行
并发程序的构建
大多数的并发程序都是通过“任务执行”来构造的,任务通常是一些抽象且离散的工作单元。将业务逻辑抽象城一个个的任务,交给不同线程来并发执行。java中可以通过Runnable来定义任务单元,通过Thread以独立的线程执行。线程是比较宝贵的资源,需要合理的复用、管理、分配、执行。线程池管理线程使用的一个组件。
线程池
jdk的线程池使用非常简单,通过Executor的excute方法执行任务即可,至于线程如何管理取决于Executor的实现。Executor通过生产者-消费者模式,将任务和执行策略进行了解耦。
public interface Executor { void execute(Runnable command); }
jdk提供了ThreadPoolExecutor作为Executor的线程池实现。该实现线程池中有几个重要的概念:固定线程数(核心线程数)、最大线程数、队列大小、线程存活时间。固定线程数创建后就不会消亡;队列是超过固定线程执行能力的任务暂存于队列;最大限线程数是超过固定线程及队列能力的提供动态扩展线程能力,在使用完后超过指定存活时间没有被使用就会被消亡。通过这几个核心变量来创建不同的线程池。
执行流程:当提交任务给线程池时,首先会判断当前线程个数是否小于核心线程池数,小于直接创建线程执行,否则将任务放入队列;如果队列也满了,那么就会看当前线程是否小于最大线程数,小于直接创建线程执行。线程池会不停从队列中取任务,超过核心线程数的线程,在存活时间后,会自动回收。
jdk提供了一些线程池和默认的配置,可以通过Executors中的静态工厂方法来创建线程池。
方法 | 说明 |
newFixedThreadPool | 创建一个固定长度的线程池,固定线程数n;最大线程数n;LinkedBlockingQueue队列 |
newCachedThreadPool | 创建一个可扩展的线程池,固定线程数0;最大线程数Integer.MAX_VALUE;存活60秒;SynchronousQueue队列 |
newSingleThreadExecutor | 创建一个单线程的线程池,固定线程数1;最大线程数1;LinkedBlockingQueue队列 |
newScheduledThreadPool | 创建一个固定长度的线程池,并且以延时或定时执行 |
为了解决执行服务的生命周期问题,Executor扩展了ExecutorService接口,并且提供了一些生命周期管理的方法。ExecutorService有3种状态,运行、关闭、终止。在初始创建的时候,处于运行状态;当调用shutdown后,不再接受新任务,已提交的任务会执行完,此时处于关闭状态;当任务执行完,则处于终止状态。
shutdownNow与shutdown的不同在于,shutdownNow不会等待已提交的任务执行完,而是立即尝试停止执行的任务和在队列中的任务,并且返回等待中的任务。
awaitTermination会执行shutdown并且阻塞,直到处于终止状态。
Callable与Future
Executor使用Runnable作为其任务表示时,有一个缺陷就是Runnable没有返回值并且不能抛出受检查的异常。这使得我们需要通过一些共享资源来同步线程执行结果。
Callable可能是一个对任务更全面的定义。
public interface Callable<V> { V call() throws Exception; }
ExecutorService同样扩展了获取返回值和执行异常的能力并且提供了取消任务的能力, <T> Future<T> submit(Callable<T> task)方法提交一个任务,并返回一个Future。本质上Future的实现就是一个共享对象,线程执行完自己的任务后会将结果设置在Future的实现中,并且进行了并发控制,实现线程安全。
Future的get方法会一直阻塞直到任务完成,返回结果;如果执行抛出异常,则会将异常封装成ExecutionException重新抛出;如果任务被取消,则会抛出CancellationException;cancel(boolean mayInterruptIfRunning)尝试取消已提交的任务,如果任务还未执行,则任务将被取消并返回true,如果任务已经执行,根据mayInterruptIfRunning尝试中断。已经执行或者已经取消的任务将返回false。
此外ExecutorService提供了批量执行的方法,invokeAll提交一个Callable集合,并按顺序返回Future集合。invokeAny提交一个Callable集合,任意一个任务执行完成即返回。
线程池的管理
线程池的各项配置依赖与任务本身(任务运行时长,cpu使用率等)以及任务依赖的各项资源(cpu、内存、依赖任务等),(从线程的角度来看,线程还是串行执行任务的),最终的目的还说让cpu达到最高的利用率,并且减少任务切换带来的额外损耗。