任务执行
1、线程中执行任务
1.1、串行地执行任务
在应用程序中存在多种调度策略,最简单的一种是在单个线程中串行地执行各个任务。
class SingleThreadWebServer{
public static void main(String[] args)throws IOException{
ServerSocked socket = new ServerSocket(80);
while(true){
Socket connection = socket.acept();
handleRequest(connection);
}
}
}
这种调度策略在访问量较低时,时可以应付的。但是这种机制无法提供高吞吐率和快速响应性。
1.2、为任务创建线程
通过为每一个请求任务创建一个新的线程来提供服务,从而实现更高的响应性。
class SingleThreadWebServer{
public static void main(String[] args)throws IOException{
ServerSocked socket = new ServerSocket(80);
while(true){
final Socket connection = socket.accept();
Runnable task = new Runnable(){
public void run(){
handleRequest(connection);
}
}
new Thread(task).start();
}
}
}
在正常负载下,“为每个任务分配一个线程”的方法能提升串行执行的性能。只要请求的到达率不超过服务器的请求处理能力,那么这种方法就可以带来更高的吞吐率和更快的响应性。
1.3、无线创建线程的代价
- 线程生命周期的开销非常高(新线程将消耗大量的计算资源)
- 资源消耗(消耗系统资源,尤其是内存)
- 稳定性(最大线程数量,不同的平台可能不同)
综上所述,虽然效果有改进,但是都不尽人意。
2、Executor框架
Executor基于生产者-消费者模式,提交任务的操作相当于生产者,执行任务的线程相当于消费者。
2.1、示例:基于Executor的Web服务器
基于前边的代码,这里创建一个固定长度的线程池,可以容纳100个线程。
class TaskExecutionWebServer{
private static fianl int NTHREADS = 100
private static fianl Executor exec = Executor.newFixedThreadPool(NTHREADS);
public static void main()throws IOExeception{
ServerSocket socket = new ServerSocket(80);
while(true){
final Socket connection = socket.accept();
Runnable task = new Runnable(){
public void run(){
handleRequest(connection);
}
};
exec.execute(task);
}
}
}
线程池是指管理一组同构工作线程的资源池。工作队列中保存着所有等待执行的任务,工作者线程的任务很简单:从工作队列中获取一个任务,执行任务,人后返回线程池并等待下一个任务。这里的线程是重复利用的,不是每次新建的。
创建线程池的方法:
- newFixedThreadPool 创建固定长度的线程池。
- newCachedThreadPool 创建可缓存的线城市
- newSingleThreadExecutor创建单个Executor
- newScheduledThreadPool 创建固定长度的线程池,以延迟或者定时的方式执行任务。
2.2、执行策略
- 在什么(what)线程中执行。
- 任务按照什么(what)顺序执行(FIFO?)
- 有多少个(how many)任务能并发执行。
- 在队列中有多少个(how many)任务等待执行。
- 如果系统过载需要拒绝任务,那么应该选择(which)哪一个任务?如何(how)通知应用程序有任务被拒绝。
- 在一个任务执行之前和之后应该进行哪些(what)动作。
2.3、生命周期
由于Executor以异步方式来执行任务,因此在任何时刻,之前提交的任务的转台不是立即可见的。有些任务可能已经完成,有些可能正在执行,而其他的任务可能在队列中等待执行。当应用程序关闭时,可能采用最平缓的关闭方式(完成所有已经启动的任务,并且不再接受任何新的任务),也可能采用最粗暴的方式(关闭电源),为了解决生命周期的问题,Executor扩展了ExecutorService接口,添加了一下生命周期的管理方法。具体的可以查看ExecutorService。
//平缓的关闭方式
void shutdown();
//粗暴的关闭方式
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();//轮训是否终止
boolean awaitTermination(long timeout,TimeUnit unit);
....
2.4、周期任务
Timer类负责管理延迟任务以及周期任务,但是存在一些缺陷,应该考虑使用ScheduledThreadPoolExecutor来替代它。如果要构建自己的调度服务,那么可以使用DelayQueue。它实现了BlockingQueue,并为ScheduledThreadPoolExecutor提供调度功能。
3、优化实例
假设现在有一个页面需要我们去渲染,这个页面包含HTML标签,预定大小的图片和URL。
3.1、串行页面渲染
最简单的处理方法就是对HTMl页面进行串行处理。
public class SingleThreadRenderer{
void renderPage(CharSquence source){
renderText(source);
List<ImageData> imageData = new ArrayList<ImageData>();
for(ImageInfo imageInfo : scanForImageInfo(source)){
imageData.add(imageInfo.downloadImage());
}
for(ImageData data:imageData){
renderImage(data);
}
}
}
这种方式的大部分时间都是在等待I/O操作执行完成,CPU在这期间几乎不做任何工作。因此如果能够分开执行,将会获得更高的效率。
3.2、携带结果的Callable和Future
Executor框架中,已提交但未开始的任务可以取消,但是对于那些已经开始执行的任务,只有他们能响应中断时,才能取消。
为了使页面的渲染速度提高,首先将渲染过程分解为两个任务,一个是渲染所有的文本,另一个是下载所有的图像。(因为一个是CPU密集型,一个是I/O密集型)Callable和Future有助于表示这些协同人物之间的交互。
public class FutureRenderer{
private final ExecutorService executor =...;
void renderPage(CharSequence source){
final List<ImageInfo> imageInfos = scanForImageInfo(source);
Callable<List<ImageData>> task =
new Callable<List<ImageData>>() {
public List<ImageData> call(){
List<ImageData> result = new ArrayList<ImageData>();
for(ImageInfo imageInfo:imageInfos){
result.add(imageInfo.downloadImage())
}
return result;
}
}
Future<List<ImageData>> future = executor.submit(task)
renderTask(source);
try{
//如果任务完成,get会抛出异常;如果没完成,get将阻塞。
List<ImageData> imageData = future.get();
for(ImageData:imageData)
renderImage(data);
}catch(InterruptedException e){
//重新设置线程中断状态
Thread.currentThread().interrupt();
//由于不需要结果,取消任务
future.cancel(true);
}catch(Exeception e){
}
}
}
3.3、CompletionService
如果Executor提交一组任务,希望得到计算结果,可以保留与每个任务关联的Future,然后反复使用get,同时将timeout指定为0,通过轮询判断任务是否完成,但是这种凡是过于繁琐,这里哟更好的方法。
CompletionService将Executor和BlockingQueue融合在一起。你可以将Callablle任务提交给他,然后使用队列操作的take方法和poll等方法来获取已经完成的结果。
总结
Executor框架将任务提交与执行策略解耦开来,同时还支持多种不同类型的执行策略。当需要创建线程来执行任务时,可以考虑使用Executor。要想在将应用程序分解为不同的任务时获得最大好处,必须定义清晰的任务边界。