大多数并发应用程序是围绕任务(task)进行管理的。任务是抽象、离散的工作单元。把一个应用程序的工作分离到任务中,可以简化程序管理,因此可以在不同事务间划分自然的分界线,在程序出现错误时进行恢复,也有利于提高程序的并发性。
一、线程执行策略
围绕着任务来管理应用程序时,第一步要指明一个清晰的任务边界,理想情况下,任务是独立的活动:工作不依赖其他任务状态、结果或者边界效应,有利于并发。
在正常负载下,服务器应用程序应该兼具良好吞吐量和快速响应性,在负荷过载时平缓地劣化,而不是负载一高就简单以失败告终。这样,我们需要一个清晰的任务边界,配合一个明确的任务执行策略。大多数服器应用程序都选择单独客户请求作为任务边界。
1.顺序执行任务
单一线程顺序执行任务,一次只能处理一个请求,主线程不断接受连接与处理相关请求交替进行,直到主线程完成当前请求,再次调用accept,此前新的任务都必须等待。顺序化处理请求几乎不能为服务器应用程序提供良好吞吐量或快速的响应性。
class SingleThreadWebServer{
public static void main(String[] args) throws IOException{
ServerSocket socket = new ServerSocket(80)
while(true){
Socket connection = socket.accept();
handleRequest(connection);
}
}
2.显式地为任务创建线程
为了提供更好的响应性,可以为每个服务请求创建一个新线程。主线程序不断交替运行接受外部连接请求与转发请求。主循环为每个连接都创建一个新线程处理请求,不在主循环内部处理。
优点
- 执行任务负载脱离主线程,主线程可以在完成前面的请求之后接受新请求,提高响应性
- 并行性,多个请求同时得到任务,程序吞吐量提高
- 任务处理代码必须线程安全,多个任务会并发调用
中等强度负载水平,”每任务每线程“方法是对顺序化执行良好改进。当请求速度超出服务器请求处理能力,这个方法可以带来更快的响应量和更大的吞吐量。
如果无限制创建线程,缺点:
- 线程生命周期开销,创建线程需要时间,带来处理延迟,需要JVM和操作系统之间进行相应处理活动。如果请求是频繁且轻量,为每个请求创建一个新线程会消耗大量计算资源
- 资源消耗量,活动线程会消耗系统资源,尤其是内存。如果可运行线程多于可用处理器,线程会空闲。大量空闲线程占用更多内存,给垃圾回收带来压力
- 稳定性,应该限制创建线程数目。
class ThreadPerTaskWebServer{
public static void main(String args) throws IOException{
ServerSocket socket - new ServerSocket(80);
while(true){
final Socket connection = socket.accept();
Runnable task = new Runnable(){
public void run(){
handleRequest(connection);
}
};
new Thread(task).start();
}
}
}
二、Executor
任务是逻辑上的工作单元,线程是使任务异步执行的机制。在java类库中,任务执行的首要抽象不是Thread,而是Executor。
public interface Executor{
void execute(Runnable command)
}
Executor只是简单的接口,但它却为一个灵活而强大的框架创造基础,用于异步任务执行,支持很多不同类型任务执行策略。为任务提交和任务执行之间解耦提供标准方法,使用Runnable描述任务提供调用方式,实现对生命周期支持及钩子函数,比如添加统计收集、应用程序管理机制和监视器等扩展。
Executor基于生产者-消费者模式,提交任务执行者是生产者(产生待完成的工作单元),执行任务的线程是消费者(消耗掉这些工作单元),Executor是实现生产者-消费者设计的最简单方式。
class TaskExecutionWebServer{
private static final int NTHREADs = 100;#实现定长线程池,容纳100个线程
private static final Executor exec = Executors.newFixedThreadPool(NTHREADS);
while(true){
final Socket = new ServerSocket(80);
Runnanle task = new Runnable(){
handleRequest(connection);
};
exec.execute(task);
}
}
1.执行策略
将任务的提交与任务的执行进行解耦,可以简单为类给定类执行执行策略,执行策略是资源管理工具,也保证后续的修改不太困难。一个执行策略知名任务执行"what,where,when,how"几个因素
- 任务在做什么线程(what)中执行?
- 任务以什么(what)顺序(FIFO,LIFO,优先级)执行?
- 可以有多少个(how many)任务并发执行?
- 可以有多少个(how many)任务进入等待队列?
- 如果系统过载,需要放弃一个任务,应该挑选哪一个(which)任务?如何(how)通知应用程序知道这一切?
- 在一个任务的执行前和执行后,应该做(what)处理?
2.线程池
线程池管理一个工作者线程的同构池,线程池与工作队列紧密绑定。工作队列,持有所有等待执行的任务。可以重用存在线程,而不是创建新线程,处理多请求时抵消线程创建、消亡产生开销,提高响应性。
调用Executors静态工厂方法创建线程池
- newFixedThreadPool:定长线程池
- newCachedThreadPool:可缓存线程池,当前线程池长度超过处理需要时,灵活收回空闲线程,需求增加时,灵活添加新线程
- newSingleThreadExecutor:单线程池,创建唯一工作者线程执行任务,线程异常结束,另一个取代
- newShceduledThreadPool:定长线程池,支持定时周期性执行任务,类似于Timer
3.生命周期
Executor是异步执行任务,任何时间内,之前提交的任务状态都是不立即可见的。这些任务,有的可能已经完成,有的可能正在运行,有的还在队列中等待执行。ExecutorService接口拓展了Executor,添加用于生命周期管理的方法。
public interface ExecutorService extends Executor{
void shutdown();
List<Runnable> shutdownNow();
boolean isShutDown();
boolelan isTerminated();
boolean awaitTermination(long timeout,TimeUnit unit) throws InterruptedExecution;
}
生命周期有三种状态:运行(running)、关闭(shutting down)、终止(terminated)。shutting down启动平缓关闭过程,停止接受新任务,等待已经提交的任务完成--包括尚未执行的任务,shutdownNow启动强制关闭过程,尝试取消所有运行中的任务和排在队列中尚未开始的任务。
三、可强化的并行性
Executor框架让制定策略变得简单,想要使用Executor,必须将任务描述为Runnable。大多数服务器应用程序,都存在一个明显的任务边界:单一客户请求。但是一个单一客户请求内部仍然可以进一步细化并行性。
1.可携带结果的任务:Callable和Future
Executor框架使用Runnable作为任务的基本表达形式,runnable只是个相当有限的抽象,虽然能产生边界效应,比如记录日志文件或者将结果存入一个共享的数据结构,但run不能返回一个值或抛出异常
很多任务都会引起严重计算延迟---执行数据库查询,从网络上获取资源,进行复杂的计算。对于这些任务,Callable是更佳抽象,主进入点(main entry point) call等待返回值,并为可能抛出的异常预先做好准备。Executors包含了一些工具方法,可以把其他类型的任务封装成一个Callable,比如Runnable和java.security.PriviledgeAction。
Runnable和Callable描述的是抽象的计算性任务,这些任务通常有限:有明确的开始点,而且不会最终结束。一个Executor执行的任务生命周期有4个阶段:创建、提交、开始、完成。在Executor框架中,可以取消已经提交尚未开始的任务,已经开始的任务,只有相应中断,才可以取消。
Future描述了任务的生命周期,并提供了相关的方法来获得任务的结果、取消任务以及检查任务是否已经完成或被取消。Future暗示任务的生命周期是单向的,不能后退。一旦完成,永远停留在完成状态上。任务状态决定了get方法行为,如果任务已经完成,get会立即返回抛出一个Execption,如果任务没有完成,get会阻塞直到它完成。如果任务抛出异常,get会将该异常封装为ExecutionException,然后重新抛出。如果任务被取消,get抛出CancellationExcetion。当抛出了ExecutionException,可以用getCause重新获得被封装的原始异常。
有很多种方法可以创建一个描述任务的Future。ExecutionService中所有的submit方法都返回一个Future,可以将一个Runnable或一个Callable提交给executor,然后得到一个Future,重新获得任务执行结果,或者取消任务。也可以显式地给定的Runnable或Callable实例化一个FutureTask。