一,在线程中执行任务
a, 串行地执行任务
最简单的策略就是在单个线程中串行地执行各项任务。但串行处理机制通常都无法提供高吞吐率或快速响应性。
b, 显示地为任务创建线程
通过为每个请求创建一个新的线程来提供服务,从而实现更高的响应性。但为每个任务分配一个线程也存在一些缺陷:
1,无限创建线程的不足:
- 线程生命周期的开销非常高。线程的创建过程需要时间,这就延迟了请求的处理,并且需要JVM和操作系统提供一些辅助操作。
- 资源消耗。如果可运行线程数量多于可用处理器的数量,那么有些线程会闲置就会占用许多内存,如果大量线程在竞争CPU还会产生其他的性能消耗。
- 稳定性。在可创建线程的数量上有一个阈值,这个阈值随着平台不同而不同,并且受多个因素制约,包括JVM的启动参数、Thread构造函数中请求的栈大小,以及底层操作系统对线程的限制等。如果超过这个限制,就很可能有OOM异常。
在一定的范围内,增加线程可以提高系统的吞吐量,但如果超过这个范围,再创建更多的线程只会降低程序的执行速度,如果过多地创建线程,整个系统就有可能崩溃。要想避免这种危险,就应该对应用程序可以创建的线程数量进行限制,并且全面地测试应用程序,从而确保线程数量达到限制时,程序也不会耗尽资源。
2,Executor框架
任务是一组逻辑工作单元,而线程则是使任务异步执行的机制。前边介绍了两种在线程中执行任务的策略,即把所有任务放在单个线程中串行执行,以及将每个任务放在各自的线程中执行。这两种方式都存在一些严格的限制:串行执行的问题在于其糟糕的响应性和吞吐量,而“为每个任务都分配一个线程”的问题在于资源管理的复杂性。
所以就有了线程池,线程池简化了线程的管理工作,并且java.util.concurrent提供了一种灵活的线程池作为Executor框架的一部分。在java类库中,任务执行的不是Thread,而是Executor:
public interface Executor {
void execute(Runnable command);
}
Executor款家提供了一种标准的方法将任务的提交过程与执行过程解耦开来,并用Runnable来表示任务。Executor的实现还提供了对生命周期的支持,以及统计信息收集/应用程序管理机制和性能监视等机制。Executor基于生产者-消费者模式,提交任务的操作相当于生产者,执行任务的线程相当于消费者。
1,基于Executor的Web服务器
class TaskExecutorWebServer {
private static final int NTHREAD = 100;
private static final Executor exe = Executors.newFixedThreadPool(NTHREAD);
public static void main(String[] args) {
ServerSocket socket = new ServerSocket(80);
while(true) {
final Socket connection = socket.accept();
Runnable task = new Runnable() {
public void run() {
handleRequest(connection);
}
};
exec.execute(task);
}
}
}
在TaskExecutionWebServer中,通过使用Executor,将请求处理任务的提交与任务的实际执行解耦开来。TaskExecutionWebServer使用了一个带有有界线程池的Executor。通过execute方法将任务提交到工作队列中,工作线程反复地从工作队列中取出任务并执行它们。
也可以很容易地将上例修改为ThreadPerTaskWebServer的行为,只需要使用一个为每个请求都创建新线程的Executor。
public class ThreadPerTaskExecutor implements Executor {
public void execute(Runnable r) {
new Thread(r).start();
}
}
还可以编写一个Executor使ThreadPerTaskExecutor的行为类似于单线程的行为:
public class WithinThreadExecutor implements Executor {
public void execute(Runnable r) {
r.run();
}
}
各种执行策略都是一种资源管理工具,最佳策略取决于可用的计算资源以及对服务质量的需求。通过限制并发任务的数量,可以确保应用程序不会由于资源耗尽而失败,或者由于在稀缺资源发生竞争而严重影响性能。通过将任务的提交于任务的执行分离开来,有助于在部署阶段选择与可用硬件资源最匹配的执行策略。
***************************************************
每当看到下面形式的代码时,并且希望获得一种更灵活的执行策略时,考虑使用Executor来代替Thread:
new Thread(runnable).start();
***************************************************
2, 线程池
线程池是指管理一组相同工作线程的资源池。线程池是与工作队列密切相关的,其中在工作队列中保存了所有等待执行的任务。工作线程的任务很简单: 从工作队列中获取一个任务,执行任务,执行完后返回线程池并等待下一个任务。
“在线程池中执行任务”比“为每个任务分配一个线程”优势更多。通过重用现有的线程而不是创建新线程,可以减少在线程创建与销毁的开销。另一个好处是请求到来时,不会再因为要等待线程创建而延迟,也就提高了响应性。通过适当调整线程池的大小,可以创建足够多的线程以便处理器保持忙碌状态,同时还可以防止过多线程互相竞争资源而使应用程序耗尽内存。
类库提供了一个灵活的线程池以及一些有用的默认配置。可以通过调用Executors中的静态工厂方法之一来创建一个线程池:
- newFixedThreadPool:创建一个固定长度的线程池,每当提交一个任务时就创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不再变化。(如果某个线程由于发生了未预期的Exception而结束,那么线程池会补充一个新的线程)。
- newCachedThreadPool:创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求时,那么将回收空闲的线程,而当需求增加时,则可以添加新的线程,线程池的规模不存在任何限制。
- newSingleThreadExecutor:是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建另一个线程来替代。它能确保依照任务在队列中的顺序来串行执行。
- newScheduledThreadPool:创建了一个固定长度的线程池,而且以延迟或定时的方式执行任务,类似于Timer。
从“为每个任务分配一个线程”策略变为基于线程池的策略,将对应用程序的稳定性产生重大影响:Web服务器不会再在高负载情况下失败。由于服务器不会创建数千个线程来争夺有限的CPU和内存资源,因此服务器的性能将平缓地降低。通过使用Executor,可以实现各种调优/管理/监视/记录日志/错误报告和其他功能,如果不使用任务执行框架,那么要增加这些功能是非常困难的。
3,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);
}
}
}
4, 延迟任务与周期任务
Timer类负责管理延迟任务以及周期任务,然而,Timer存在一些缺陷,因此应该考虑使用ScheduledThreadPoolExecutor来代替它,可以通过ScheduledThreadPoolExecutor的构造函数或Executors.newScheduledThreadPool工厂方法来创建该类的对象。
三,找出可利用的并行性
1,带结果的任务Callable和Future
Executor使用Runnable作为其基本的任务表示形式,但是Runnable是一种有很大局限的抽象,虽然run能写入到日志文件或者将结果放入到某个共享的数据结构,但它不能返回一个值或抛出一个受检查的异常。
许多任务实际上都是存在延迟的计算-- 执行数据库查询,从网络上获取资源,或者计算某个复杂的功能。对于这些任务,Callable是一种更好的抽象:它认为主入口点(即call)将返回一个值,并可能抛出一个异常。
Runnable和Callable描述的都是抽象的计算任务。这些任务通常都是有范围的,即都有一个明确的起点,并且最终会结束。Executor执行的任务又四个生命周期阶段:创建/提交/开始/完成。由于有些任务可能要执行很长的时间,因此通常希望能够取消这些任务。在Executor框架中,已提交但尚未开始的任务可以取消,但对于那些已经开始执行的任务,只有当它们响应中断时,才能取消。
Future表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等。在Future规范中包含的隐含意义是,任务的生命周期只能前进,不能后退。当某个任务完成后,它就永远停留在“完成”状态上。
public interface Callable<V> {
V call() throws Exception;
}
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException, CancellationException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, CancellationException, TimeoutException;
}
可以通过许多方法创建一个Future来描述任务。ExecutorService中的所有submit方法都将返回一个Future,从而将一个Runnable或Callable提交给Executor,并得到一个Future用来获得任务的执行结果或者取消任务。还可以显式地为某个指定的Runnable或Callable实例化一个FutureTask。
private ExecutorService executorService = Executors.newCachedThreadPool();
public FavoriteResponse recommendAndFavoriteQuery(Object signals, String id) {
CountDownLatch doneSignal = new CountDownLatch(2);
Future<List<RecommendTool>> futureRecommend = recommendQueryFuture(signals, id, doneSignal);
Future<List<Favorite>> futureFavorite = favoriteQueryFuture(id, doneSignal);
List<Favorite> favorites = new ArrayList<>();
List<RecommendTool> recommendedTools = new ArrayList<>();
try {
doneSignal.await(60, TimeUnit.SECONDS);
favorites = futureFavorite.get();
recommendedTools = futureRecommend.get();
} catch (InterruptedException e1) {
logger.error(e1.toString());
} catch (ExecutionException e) {
logger.error(e.toString());
}return new FavoriteResponse(favorites, limitRecommendSize(recommendedTools), ResponseMessage.SUCCESS);
}
private Future<List<RecommendTool>> recommendQueryFuture(Object signals, String ssoid, CountDownLatch doneSignal) {
return executorService.submit(() -> {
logger.info("Getting user's favorites tools.");
List<RecommendTool> recommendTools = service.getRecomendTools();
doneSignal.countDown();
return recommendTools;
});
}
使用Future的步骤:
a,新建ExecutorService
ExecutorService executorService = Executors.newCachedThreadPool();
b, 新建Callable
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); } return result; } };
c, 提交任务,获得Future
Future<List<ImageData>> future = (Future) executorService.submit(task);
d,调用get,获取结果,
try {
List<ImageData> imageData = future.get();
for (imageData data : imageData) {
RenderableImage(data);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
future.cancel(true);
} catch (ExecutionException e) {
throw launderThrowable(e.getCause());
}
get方法的行为取决于任务的状态(尚未开始,正在执行,已完成)。如果任务已经完成,那么get会立即返回或者抛出一个Exception,如果任务没有完成,那么get将阻塞并直到任务完成。如果任务抛出了异常,那么get将该异常封装为ExecutionException并重新抛出。如果任务被取消,那么get将抛出CancellationException。
2, 为不同类型的任务实行并行化存在的局限
如果将两个任务A和B分配给两个工人,但A的执行时间是B的10倍,那么整个过程也只能加速9%。最后,当在多个工人之间分解任务时,还需要一定的任务协调开销:为了使任务分解能提高性能,这种开销不能高于并行性实现的提升。
所以只有当大量互相独立且同构的任务可以并发进行处理时,才能体现出将程序的工作负载分配到多个任务中带来的真正性能提升。
3,CompletionService:Executor和BlockingQueue
如果向Executor提交了一组计算任务,并且希望在计算完成后获得结果,那么可以保留与每个任务关联的Future,然后反复使用get方法,同时将参数timeout指定为0,从而通过轮训来判断任务是否完成。这种方法虽然可行,但有些繁琐。幸好有CompletionService(完成服务)。
CompetionService将Executor和BlockingQueue的功能融合在一起,可以将Callable任务提交给它来执行,然后使用类似于队列操作的take和poll等方法来获得已完成的结果,而这些结果会在完成时被封装为Future。ExecutorCompletionService实现了CompletionService,并将计算部分委托给一个Executor。
public interface CompletionService<V> {
Future<V> submit(Callable<V> task);
Future<V> submit(Runnable task, V result);
Future<V> take() throws InterruptedException;
Future<V> poll();
Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException;
}
public class ExecutorCompletionService<V> implements CompletionService<V> {
private final Executor executor;
private final AbstractExecutorService aes;
private final BlockingQueue<Future<V>> completionQueue;
private class QueueingFuture extends FutureTask<Void> {
QueueingFuture(RunnableFuture<V> task) {
super(task, null);
this.task = task;
}
protected void done() { completionQueue.add(task); }
private final Future<V> task;
}
private RunnableFuture<V> newTaskFor(Callable<V> task) {
if (aes == null)
return new FutureTask<V>(task);
else
return aes.newTaskFor(task);
}
private RunnableFuture<V> newTaskFor(Runnable task, V result) {
if (aes == null)
return new FutureTask<V>(task, result);
else
return aes.newTaskFor(task, result);
}
public ExecutorCompletionService(Executor executor) {
if (executor == null)
throw new NullPointerException();
this.executor = executor;
this.aes = (executor instanceof AbstractExecutorService) ?
(AbstractExecutorService) executor : null;
this.completionQueue = new LinkedBlockingQueue<Future<V>>();
}
public ExecutorCompletionService(Executor executor,
BlockingQueue<Future<V>> completionQueue) {
if (executor == null || completionQueue == null)
throw new NullPointerException();
this.executor = executor;
this.aes = (executor instanceof AbstractExecutorService) ?
(AbstractExecutorService) executor : null;
this.completionQueue = completionQueue;
}
public Future<V> submit(Callable<V> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<V> f = newTaskFor(task);
executor.execute(new QueueingFuture(f));
return f;
}
public Future<V> submit(Runnable task, V result) {
if (task == null) throw new NullPointerException();
RunnableFuture<V> f = newTaskFor(task, result);
executor.execute(new QueueingFuture(f));
return f;
}
public Future<V> take() throws InterruptedException {
return completionQueue.take();
}
public Future<V> poll() {
return completionQueue.poll();
}
public Future<V> poll(long timeout, TimeUnit unit)
throws InterruptedException {
return completionQueue.poll(timeout, unit);
}
}
使用CompletionService的步骤
a, 创建ExecutorService
private final ExecutorService executorService = Executors.newCachedThreadPool();
b, 创建ExecutorCompletionService
CompletionService<ImageData> completionService = new ExecutorCompletionService<>(executorService);
c,提交任务
List<ImageInfo> info = scanForImageInfo(source);
for(final ImageInfo imageInfo: info) { completionService.submit(new Callable<ImageData>() { public ImageData call() { return imageInfo.downloadImage(); } }); }
d,获取Future,调用get
try {
for(int i=0, n=info.size(); t<n; t++) {
Future<ImageData> future = completionService.take();
ImageData imageData = future.get();
RenderImage(imageData);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
throw launderThrowable(e.getCause());
}
4,为任务设置时限
有时候,如果某个任务无法在指定时间内完成,那么将不再需要它的结果,此时可以放弃这个任务。在支持时间限制的Future.get中支持这种需求:当结果可用时,它将立即返回,如果在指定时限内没有计算出结果,那么将抛出TimeoutException。
在使用限时任务时需要注意,当这些任务超时后应该立即停止,从而避免为继续计算一个不再使用的结果而浪费计算资源。Future如果一个限时的get方法抛出了TimeoutException,那么可以通过Future来取消任务。
Page renderPageWithAd() throws InterruptedException {
long endNanos = System.nanoTime() + TIME_BUDGET;
Future<Ad> future = exec.submit(new FetchAdTask());
Page page = renderPageBody();
Ad ad;
try {
long timeLeft = endNanos - System.nanoTime();
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;
}