思维导图:
引言:
本章的核心内容是描述如何使用多线程来执行任务,会遇到什么问题,又该如何解决。按照惯例,本章内容分为两个部分
- 理论部分:介绍了如何使用多线程,以及如何管理多线程
- 使用部分:以网页渲染为例使用多线程,并分析任务的并行性处理
一.在线程中执行任务
本节分为两部分,即使用单线程执行任务和使用多线程执行任务。
1.1 串行执行任务
执行任务最简单的策略就是在单个线程中串行的执行各项任务。以下代码中,负责处理请求的WebService每次只能处理一个请求,只有当一个请求处理完成后,才能接收新的请求并处理。当处理请求需要较长的时间时,会导致服务的相应速度相当的慢,而且串行处理网络请求不能较好的利用CPU资源。
public class SingleThreadWebServer {
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while (true) {
Socket connection = socket.accept();
handleRequest(connection);
}
}
private static void handleRequest(Socket connection) {
// request-handling logic here
}
}
1.2 并行执行任务
为了解决上述串行执行任务的不足,我们可以为每个请求都创建一个线程来处理。毫无疑问,请求的处理速度会大大提高。
public 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();
}
}
private static void handleRequest(Socket connection) {
// request-handling logic here
}
}
但是,毫无限制的创建线程会导致新的问题。比如线程创建时所导致的高开销,内存及其他资源的消耗,由于线程太多而导致的低稳定性。由于内存资源是有限的,在线程之间切换也是一项高额的开销,所以,毫无限制的创建线程不是一个好主意。最终结论就是,我们需要对线程进行管理。
二.Executor框架
对线程进行管理的工具称为线程池。java.util.concurrency包提供一个一种灵活的线程池以实现Executor框架。在Java的类库中执行任务的抽象不是Thread而是Executor。Executor基于生产者-消费者模式实现,所以向Exucutor添加任务后可能不会立即执行。
2.1 线程池
线程池具有比“为每个任务创建一个线程”更具有优势,它可以通过重用线程而不是创建线程来减少线程创建和销毁时的巨大开销,也不会有等待线程创建的延时,还可以保证线程的数量以防止内存耗尽。
那么,一般常用的线程池有哪些呢,又有什么特别的作用呢?
2.1.1 newFixedThreadPool
newFixedThreadPool将创建一个固定长度的线程池,每当提交一个任务时就创建一个线程,直到达到线程池的最大数量,而这时,线程池的规模将不再变化。
public class TaskExecutionWebServer {
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() {
public void run() {
handleRequest(connection);
}
};
exec.execute(task);
}
}
private static void handleRequest(Socket connection) {
// request-handling logic here
}
}
2.1.2 newCashedThreadPool
newCashedThreadPool将创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求时,将回收空闲的线程,而当需求增加时,则可以添加新的线程,线程的规模不存在任何限制。
2.1.3 newSingleThreadExecutor
newSingleThreadExecutor是一个单线程的Executor,它创建单个工作线程来执行任务,如何这个线程异常结束,会创建另一个线程替代。newSingleThreadExecutor也会确保依照任务在队列中的顺序执行。
2.1.4 newScheduledThreadPool
newScheduledThreadPool创建一个固定长度的线程池,而且可以用延迟或定时的方式来执行任务。
2.2 Executor的生命周期
上一小节中我们介绍了如何创建Executor,但是却没有讨论如何关闭它。JVM只有在所有的非守护线程都终止后才会退出,所以如果无法正确的关闭Executor,JVM将无法结束。
为了解决执行服务的生命周期问题,Executor扩展出了ExecutorService接口,添加了一些用于生命周期管理的方法。前文提到过,Executor是基于生产者-消费者模式的,所以添加入线程池中的任务可能有三种状态:等待执行,正在执行,执行完毕。
ExecutorService的生命周期有三种状态:运行,关闭和终止。
- 运行:ExecutorService创建即处理运行状态
- 关闭:shutdown()将进行平缓的关闭-不接受新任务的同时等待所有任务执行完毕。shutdownNow()则会进行粗暴的关 闭-尝试取消所有运行的任务,并且不会启动队列中尚未开始的任务。
- 终止:当所有任务都完成后,即进入终止状态。可以调用waitTermination()等待ExecutorService达到终止状态,也可以用isTermination()判断ExecutorService是否已经终止。
public class LifecycleWebServer {
private final ExecutorService exec = Executors.newCachedThreadPool();
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 (RejectedExecutionException e) {
if (!exec.isShutdown()) {
log("task submission rejected", e);
}
}
}
}
public void stop() {
exec.shutdown();
}
private void log(String msg, Exception e) {
Logger.getAnonymousLogger().log(Level.WARNING, msg, e);
}
void handleRequest(Socket connection) {
Request req = readRequest(connection);
if (isShutdownRequest(req)) {
stop();
} else {
dispatchRequest(req);
}
}
interface Request {
}
private Request readRequest(Socket s) {
return null;
}
private void dispatchRequest(Request r) {
}
private boolean isShutdownRequest(Request r) {
return false;
}
}
三.可利用的并行性
在本节中,我们将以网页渲染为例,介绍如何分析任务中可利用的并行性和如何处理。
网页的内容分为两步分,文本和图片,网页显示图片分两步:下载和渲染。
3.1 串行执行渲染任务
以下实例先渲染文本,然后逐个渲染图片。然后这种串行的执行方法没有充分利用CPU,是得用户看到最终页面前需要等待相当长的时间。
public abstract class SingleThreadRenderer {
void renderPage(CharSequence source) {
//渲染网页文本
renderText(source);
//获取需要加载的图片
List<ImageData> imageData = new ArrayList<ImageData>();
for (ImageInfo imageInfo : scanForImageInfo(source)) {
imageData.add(imageInfo.downloadImage());
}
//渲染网页图片
for (ImageData data : imageData) {
renderImage(data);
}
}
interface ImageData {
}
interface ImageInfo {
ImageData downloadImage();
}
abstract void renderText(CharSequence s);
abstract List<ImageInfo> scanForImageInfo(CharSequence s);
abstract void renderImage(ImageData i);
}
3.2 携带结果的任务
通过分析上述代码可知,我们需要在渲染网页文本的时候加载图片,以充分的利用CPU资源。加载图片可能需要花费较长的时间,所以我们需要一种先计算,待会使用的功能。Future可以满足我们的需要。
Future表示任务生命周期的抽象。Executor所执行的任务有四个生命周期阶段:创建,提交,开始,完成。Future提供方法以对任务进行控制,比如取消任务,获取结果等,但是,任务的执行阶段只能前进,不能后退。
public abstract class FutureRenderer {
private final ExecutorService executor = Executors.newCachedThreadPool();
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);
renderText(source);
try {
List<ImageData> imageData = future.get();
for (ImageData data : imageData) {
renderImage(data);
}
} catch (InterruptedException e) {
// 重新设置线程的中断状态
Thread.currentThread().interrupt();
// 由于不需要结果,因此取消任务
future.cancel(true);
} catch (ExecutionException e) {
throw 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);
}
3.3 及早获取计算结果
上述代码依然有可以优化的地方。在上一个例子中,我们需要等待所有的图片都加载完成后才将其一个一个的渲染到网页上。现在我们可以利用ComplitionService做到加载完一个图片就渲染一个图片,而不用等到所有图片都加载完成后才开始渲染图片。
ComplitionService是由Executor和BlockingQueue组成。其使用原理是Executor在完成一个任务后立即将任务结果放入阻塞队列中,获取结果的方法则由ComplitionService提供。
public abstract class Renderer {
private final ExecutorService executor;
Renderer(ExecutorService executor) {
this.executor = executor;
}
void renderPage(CharSequence source) {
final List<ImageInfo> info = scanForImageInfo(source);
CompletionService<ImageData> completionService = new ExecutorCompletionService<ImageData>(executor);
for (final ImageInfo imageInfo : info) {
completionService.submit(new Callable<ImageData>() {
public ImageData call() {
return imageInfo.downloadImage();
}
});
}
renderText(source);
try {
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(e.getCause());
}
}
interface ImageData {
}
interface ImageInfo {
ImageData downloadImage();
}
abstract void renderText(CharSequence s);
abstract List<ImageInfo> scanForImageInfo(CharSequence s);
abstract void renderImage(ImageData i);
}