第六章 围绕任务来说
1、在线程中执行任务
任务要有自己明确的边界,之间相互独立。任务并不依赖其他任务的状态、结果或边界,尽量别有耦合。当负荷过载应当逐渐降低效率,而不是直接失败。
(1)、单线程的执行任务。碰到阻塞不仅会推迟当前请求的完成,还会彻底阻止等待中的请求被处理。阻塞时间过长用户将认为服务器不可以。服务器资源利用率相当低。但是能保证数据的完整性和原子性。并且实现任务也会简单很多。
(2)、在任务中创建多线程。更好的利用服务器的资源,给用户更快的反映速度,任务处理的代码必须保证线程安全,调试费劲。
(3)、某些情况下必须用到多线程,那么为了更好的利用服务器资源,无限制的创建线程是否好?不好!!线程的生命周期非常高,创建与销毁都需要时间,例如:服务器应用程序,请求频率特别高和执行过程是轻量级,那么为每个请求创建一个新线程将消耗大量资源。活跃的线程会消耗内存,所以要是活跃的线程多于可用处理器的数量,那么将会有些内存限制并消耗着内存。稳定性,线程越多被限制的因素就越多,越不稳定。所以在一定范围内增加线程会提高性能反映时间,超出这个范围只会降低程序的运行速度。
串行和并行都存在一些限制:串行在于糟糕的响应性和吞吐量、并行在于资源的管理和程序的复杂度上
2、Executor框架
一个很灵活好用的框架,比Thread有更好的管理、伸缩、拓展、复杂可见度。所对应的他的复杂度要比Thread更加复杂。
Executor是基于生产者-消费者模式:提交任务相当于生产者,执行任务相当于消费者。还提供了对生命周期的支持、统计信息的收集应用程序管理机制和性能监视等
1、基于线程池的web服务器
public class TaskExecutionWebService {
private static final int NTHREADS =100;
private static final Executor exec =
Executors.newFixedThreadPool(NTHREADS);
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while(true){
final Socket connection = socket.accept();
Runnable task = new Runnable() {
@Override
public void run() {
handleRequest(connection);
}
};
exec.execute(task);
}
}
}
2、执行策略
通过解耦任务的提交和执行,就可以为任务添加或修改执行策略。
例如:在什么线程中执行任务、有多少个任务可以并发的执行、按照那种顺序执行等等。
可以自己定义需要的策略,最佳的策略永远取决与计算资源以及对服务质量的需求。
通过Executor执行的任务可以获得比Thread更加零活的执行策略。
3、线程池
使用线程池的好处。
(1)、通过重用现有的线程池,减少性能上的开销。
(2)、当请求到达时,线程通常已经建好可以直接使用,从而提高响应性。
(3)、适当的调整线程池的大小,使处理器处于忙碌的状态,还能防止多线程相互竞争资源导致内存耗尽。
实现线程池的4种方式。
(1)、newFixedThreadPool,创建一个固定长度的线程池,每提交一个任务会创建一个线程,直到最大。
(2)、newCachedThreadPool,创建一个可缓存的线程池,此线程池当前线程最大数大于正在处理任务的线程数,将回收空闲的线程。当要处理的任务增加了,则可以添加新的线程,数量不存在限制。
(3)、newSingleThreadExecutor,是个单线程的Executor,如果有异常结束会创建另一个线程来代替他。能确保任务按照任务在队列中的顺序串行执行。例如:FIFO、LIFO、优先级
(4)、newScheduledThreadPool,创建一个固定长度的线程池,而且以延时或定时方式执行任务,类似于Timer
newFixedThreadPool和newCachedThreadPool返回通用的ThreadPoolExecutor
4、Executor生命周期
Executor执行的任务有4个生命周期:创建、提交、开始、完成。
在Executor框架中,已提交但尚未开始的任务可以取消,但对于那些已经开始执行的任务只有当他们能响应中断时,才能取消。取消一个已完成的任务不会有任何影响。
Executor的实现通常会创建线程来执行任务,但JVM只有在所有非守护线程全部终止之后才会退出,如果无法正确关闭Executor,那么JVM将无法结束。
由于Executor以异步方式来执行任务,因此在任何时刻,之前提交任务的状态不是立即可见的。有些任务可能已经完成,有些可能正在运行,而其他的任务可能在队列中等待执行。当关闭应用程序时,可能采用平缓的方式(完成所有已经启动的任务,并且不再接受任何新的任务),也可能采用粗暴方式(直接所有都关掉)。Executor视为应用程序提供服务的,因此它们也是可关闭的,并把在关闭操作中受影响的任务的状态返回给应用程序。
为了解决执行任务的生命周期问题,ExecutorService接口扩展了Executor,添加了一些用于生命周期管理的方法:
public interface ExecutorService extends Executor {
void shutdown();
List<Runnable> shutDownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
...
}
ExectuorService的生命周期有三种状态:运行、关闭和已终止。ExecutorService在创建时处于运行状态,shutdown方法执行优雅地关闭:不再接受新的任务,同时等待已经提交的任务执行完成–包括那些还未开始执行的任务。shutdownNow方法执行粗暴的关闭:它将尝试取消所有运行中的任务,并且不再启动队列中尚未开始执行的任务。
例如支持关闭操作的Web服务器:
class LifecycleWebServer {
private final ExecutorService exec = ...;
public void start() throws IOException {
ServerSocket socket = new ServerSocket(80);
while(!exec.isShutdown()) {
try {
final Socket conn = socket.accept();
exec.execute(new Runnable() {
public void run() {
handleRequest(conn);
}
} );
} catch (RejectedExecutonException e) {
if (!exec.isShutdown()) {
log("task submission rejected", e);
}
}
}
}
public void stop() {
exec.shutdown();
}
void handleRequest(Socket connection) {
Request req = readRequest(connection);
if (isShutdownRequest(req)) {
stop();
} else {
dispatchRequest(req);
}
}
}
5、延迟任务与周期任务
Timer可以实现管理延迟任务和周期任务,但是有缺陷(Timer在执行所有任务时只会创建一个线程并且基于绝对时间,因此对系统的时间变化很敏感),可以使用ScheduledThreadPoolExecutor来代替(只支持相对时间),他可以提供多个线程来执行相关任务。例如:TimerTask1需要每10秒执行一次,TimerTask2需要40秒执行一次,那么执行完40秒任务后可能会快速执行4次或彻底丢失4次调用。
Timer另一个问题是,Timer不捕获异常,当程序抛出未检查的异常将终止线程,也不会恢复线程的执行,会错误的认为整个Timer都被取消。
3、找出并行的可行性
1、有结果的Callable与future
Executor使用的Runnable虽然能写入日志但是不能返回一个值或者抛出异常。Callable可以抛出返回一个值或抛出异常,Runnable和Callbale描述的都是抽象的计算任务。这些任务通常是有范围的即明确的起始点并且最终都会结束 。
Future表示一个任务的生命周期,并提供相应方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等。在Futre规范中包含的隐含意义是:任务的生命周期只能前进,不能后退。当某个任务完成后他就永远停留在“完成”状态上。
Callable与future接口
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
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;
}
ExecutorService中所有submit方法都会返回一个Futurn,将一个Runnable或者Callable任务传递给ExecutorService的submit方法(这里面包含了一个安全发布的过程),将返回一个Future用于获得任务的执行结果或者取消任务。
get方法的行为取决于任务的状态(尚未开始、正在运行、已完成),get方法拥有状态依赖,因而调用者不需要知道任务的状态。下面就是get方法在同步时候返回的结果。
- 如果已完成会立即返回活抛出一个Exception
- 如果没有完成,get方法将会阻塞并直到任务完成。
- 如果抛出了异常,get方法将异常封装为ExecutionException并重新抛出,可以通过getCause来获取被封装的初始异常
- 如果任务被取消,get方法会抛出CancellationException
Futureprivate final ExecutorService executor = Executors.newCachedThreadPool();
void renderPage(String source) {
final List<ImageInfo> imageInfos = scanForImageInfo(source);
//下载图片
Callable<List<ImageData>> task = new Callable<List<ImageData>>() {
@Override
public List<ImageData> call() throws Exception {
List<ImageData> list = new ArrayList<ImageData>();
for (ImageInfo imageInfo : imageInfos) {
list.add(imageInfo.download());
}
}
};
//处理图片
//将一个Runnable或者Callable任务传递给ExecutorService的submit方法,将返回一个Future用于获得任务的执行结果或者取消任务。
Future<List<ImageData>> future = executor.submit(task);
renderText(source);
try {
//get()方法会一直阻塞,直到callable的任务全部完成。
List<ImageData> list = future.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// 由于不需要结果,因此取消任务
future.cancel(true);
} catch (ExecutionException e) {
e.printStackTrace();
}
2、在异构任务并行化中存在的局限
在FutureRenderer中,我们尝试并行地执行两个不同类型的任务-下载图像与渲染页面。然而,通过对异构任务进行并行化来获得重大的性能提升是困难的。
两个人可以很好地分担洗碗的工作:其中一个人负责清洗,另一个负责烘干。然而,要将不同类型的任务平均分给每个工人并不容易。当人数增加时,如果确保他们能帮忙而不是妨碍其他人工作,或者在重新分配工作时,并不是容易的事。如果没有在相似的任务之间找出细粒度的并行性,那么这种方法的好处将减少。
当在多个工人之间分配异构的任务时,还有一个问题就是各个任务的大小可能完全不同。
当在多个工人之间分解任务时,还需要一定的任务协调开销:为了使任务分解能提高性能,这种开销不能高于并行性实现的提升。
FutureRenderer使用了两个任务,其中一个复杂渲染文本,另一个负责下载图像。如果渲染文本的速度远远高于下载图像速度(可能性很大),那么程序的最终性能与串行执行相比性能差别不大,而代码却复杂了。
虽然做了很多工作来并发执行异构任务以提高并发度,但从中得到的并发性却是十分有限的。
只有当大量相互独立且同构的任务可以并发进行处理时,才能体现出将程序的工作负载分配到多个任务中带来的真正性能提升。
3、CompletionService: Executor 与 BlockingQueue
如果想Executor提交了一组计算任务,并且希望在计算完成后得到结果,那么可以保留与每个任务关联的Future,然后反复使用get,同时将参数timeout指定为0,从而通过轮询来判断任务是否可行。
这种方法虽然可行,但很繁琐,还有一种更好的方法:完成服务(CompletionService) 。
CompletionService将Executor与BlockingQueue的功能融合在一起。你可以将Callable任务提交给它执行,然后使用类似与队列操作的take和poll等方法来获得已完成的结果。而这些结果会在完成时被封装为Future。ExecutorCompletionService实现了CompletionService,并将计算部分委托给一个Executor。
ExecutorCompletionService的实现很简单。在构造函数中创建一个BlockingQueue来保存计算完成的结果。当计算完成时,调用Future-Task中的done方法。当提交给某个任务时,该任务将首先包装为一个QueueingFuture,这是FutureTask的一个子类,然后再改写子类的done方法,并将结果放入BlockingQueue中。
private class QueueingFuture<V> extends FutureTask<V> {
QueueingFuture(Callable<V> c) { super(c); }
QueueingFuture(Runnable t, V r) { super(t, r); }
protected void done() {
completionQueue.add(this);
}
}
4、使用CompletionService实现页面渲染器
可以通过CompletionServ从两个方法来提高页面渲染器的性能:缩短总时间以及提高响应性。
为每一幅图像的下载都创建一个独立任务,并从线程池中执行它们,从而将串行的下载过程转化为并行的过程:这将减少下载的总时间。
此外,通过CompletionService中获取结果以及使每张图片在下载完成后立刻显示出来,使用户获得一个更加动态和更高响应性的用户界面。
多个ExecutorCompletionService可以共享一个ExecutorService,通过记录提交给CompletionService的任务数量,并计算出已经获得的已完成结果的数量,即使使用一个共享的Executor,也能知道已经过的了所有任务结果的时间
public abstract class Renderer {
//CompletionService将Executor与BlockingQueue的功能融合在一起
private final ExecutorService executor;
Renderer(ExecutorService executor){
this.executor=executor;
}
void renderPage(CharSequence source){
List<ImageInfo> info=scanForImageInfo(source);
//ExecutorCompletionService实现了CompletionService,并将计算部分委托给一个Executor。
CompletionService<ImageData> completionService=
new ExecutorCompletionService<ImageData>(executor);
for(final ImageInfo imageInfo:info)
//提交一个带返回值的任务,并且返回一个代表即将得到任务的结果的Future
completionService.submit(new Callable<ImageData>(){
public ImageData call(){
//CompletionService中有类似队列的操作
return imageInfo.downloadImage();
}
});
renderText(source);
try{
//通过CompletionService中获取结果以及使每张图片在下载完成后立刻显示出来
for(int t=0,n=info.size();t<n;t++){
//取得并移除已完成任务,如果没有则等待
Future<ImageData> f=completionService.take();
ImageData imageData=f.get();
renderImage(imageData);
}
}catch (InterruptedException e) {
Thread.currentThread().interrupt();
}catch (ExecutionException e) {
throw LaunderThrowable.launderThrowable(e.getCause());
}
}
interface ImageData {
}
interface ImageInfo {
ImageData downloadImage();
}
abstract void renderText(CharSequence s);
abstract List<ImageInfo> scanForImageInfo(CharSequence s);
abstract void renderImage(ImageData i);
}
5、为任务设置时限
有时候,如果某个任务无法在指定时间内完成,那么将不再需要它的结果,此时可以放弃这个任务。
例如,某个Web应用程序从外部的广告服务器上获取广告信息,但如果该应用程序在两秒内得不到响应,那么将显示一个默认的广告,这样即使不能获得广告信息,也不会降低站点的响应性能。
在支持时间限制的Future.get中支持这种需求:当结果可用时,它立即返回,如果在指定时限内没有计算出结果,那么将抛出TimeoutException。
在使用限时任务时应注意,当这些任务超时后应该立即停止,可以节省计算机的资源。要实现这个功能,可以由人任务本身来管理它的限定事件,并且在超时后中职执行或取消任务。此时可再使用Future,如果一个限时的get方法跑出了TimeoutException,那么可以通过Future取消任务。future.cancel(true)。
public class RenderWithTimeBudget {
private static final Ad DEFAULT_AD = new Ad();
private static final long TIME_BUDGET = 1000;
private static final ExecutorService exec = Executors.newCachedThreadPool();
Page renderPageWithAd() throws InterruptedException{
//返回纳秒级的时间,再加上时限
long endNanos=System.nanoTime()+TIME_BUDGET;
Future<Ad> f=exec.submit(new FetchAdTask());
//在等待广告的同时显示页面
Page page=renderPageBody();
Ad ad;
try{
//只等待指定的时间长度
long timeLeft=endNanos-System.nanoTime();
//在指定时限内获取,NANOSECONDS为时间单位
ad = f.get(timeLeft, NANOSECONDS);
}catch (ExecutionException e) {
ad=DEFAULT_AD;
}catch (TimeoutException e) {
//如果超时了,广告转为默认广告,并取消获取任务
ad=DEFAULT_AD;
f.cancel(true);
}
//为页面设置广告
page.setAd(ad);
return page;
}
Page renderPageBody() { return new Page(); }
static class Ad {
}
static class Page {
public void setAd(Ad ad) { }
}
static class FetchAdTask implements Callable<Ad> {
public Ad call() {
return new Ad();
}
}
}
6、一组Futurn更好的解决办法
考虑这样一个旅行预定门户网站:用户输入旅行的日期和其他要求,门户网站获取并显示来自多条航线,旅店或汽车租赁公司的报价。可能会调用Web服务,访问数据库,执行一个EDI事物或其他机制。
在这种情况下,不宜让页面的响应时间受限于最慢的响应时间,而应该只显示在指定时间内收到的信息。对于没有及时响应的服务提供者,页面可以忽略或者显示提示信息。
从一个公司获取报价的过程与其他公司获得报价的过程无关,因此可以将获取报价的过程当成一个任务,从而使获得报价的过程能并发执行。创建n个任务,将其提交到线程池,保留n个Futrue,并使用限时的get方法通过Future串行地获取每一个结果,这一切都很简单,但我们还可以使用一个更简单的方法——invokeAll(invoke 援引)
下面的代码使用了支持显示的invokeAll,将多个任务提交到一个ExecutorService并获得结果。
InvokeAll方法的参数为一组任务,并返回一组Future。这两个集合有着相同的结构。
InvokeAll按照任务集合中迭代器额顺序肩所有的Future添加到返回的集合中,从而使调用者能将各个Future与其表示Callable关联起来。当所有任务都执行完毕时,或者调用线程被中断时,又或者超过指定时限时,invokeAll将返回。将超过指定时限后,任何还未完成的任务都会被取消。当invokeAll返回后,每个任务要么正常地完成,要么被取消,没有正在执行的任务,而客户端可以调用get或isCancelled来判断究竟是何种情况。
private class QuoteTask implements Callable<TravelQuote>{
private final TravelCompany company;
private final TravelInfo travelInfo;
public TravelQuote call()throw Exception{
return company.solicitQuote(travelInfo);
}
}
public List<TravelQuote> getRankedTravelQuotes(
TravelInfo travelInfo, Set<TravelCompany> companies,
Comparator<TravelQuote> ranking(long time, TimeUnit unit)
throws InterruptedException {
List<QuoteTask> tasks = new ArrayList<QuoteTask>();
//为每家公司添加报价任务
for (TravelCompany company : companies)
tasks.add(new QuoteTask(company, travelInfo));
//InvokeAll方法的参数为一组任务,并返回一组Future ,用时限来限制时间
List<Future<TravelQuote>> futures =
exec.invokeAll(tasks, time, unit);
List<TravelQuote> quotes =
new ArrayList<TravelQuote>(tasks.size());
Iterator<QuoteTask> taskIter = tasks.iterator();
for (Future<TravelQuote> f : futures) {
QuoteTask task = taskIter.next();
try {
//invokeAll按照任务集合中迭代器额顺序肩所有的Future添加到返回的集合中
quotes.add(f.get());
} catch (ExecutionException e) {
quotes.add(task.getFailureQuote(e.getCause()));
} catch (CancellationException e) {
quotes.add(task.getTimeoutQuote(e));
}
}
Collections.sort(quotes, ranking);
return quotes;
}