目录
用Executor框架实现 “每任务每线程”和顺序执行这2种调度策略的服务器
延迟任务与周期任务-ScheduledExecutorService
任务和线程
下面讨论服务器使用线程这种工具来调度任务的问题。
顺序地执行任务:
/**
* SingleThreadWebServer
* <p/>
* Sequential web server
*
* @author Brian Goetz and Tim Peierls
*/
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
}
}
一个web请求的处理,可能包括执行运算,处理文件IO,发送数据库请求,都耗时较长,而且由于网络拥堵和连通性问题,服务器读取请求和写回响应都会阻塞,明显吞吐率极低,而且若这种单线程正在吹文件IO,那么CPU将处于闲置状态,明显资源利用率也极低。
所以,为每个请求任务创建线程:
/**
* ThreadPerTaskWebServer
* <p/>
* Web server that starts a new thread for each request
*
* @author Brian Goetz and Tim Peierls
*/
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
}
}
我们把它叫做“每任务每线程”的调度策略。可以看出,1.任务的负载脱离了主线程,2.可以并行处理阻塞性的任务,3.任务处理必须安全,4.吞吐率和资源利用率得到提升。
但是,1.线程的创建和关闭需要时间,如果请求是频繁的,就会消耗大量的计算资源;2.运行的线程多余可用的处理器,导致大量的空闲线程占用更多的内存,大量线程在竞争CPU时,会产生性能开销,这都消耗资源和性能;3.线程数目多会打破栈的限制,可能会抛出outofmemoryError,影响程序的稳定性,每个线程会维护2个栈,一个java层,一个native层的,而且每个线程的栈地址空间是固定的,可以预设,当你的线程越多时,就有可能突破内存的大小。这些都是不约束线程的创建带来的效果。
总结,一个线程顺序地处理所有任务产生糟糕的响应性和吞吐率,“每任务每线程”会给资源管理(内存,cpu等等)带来麻烦。所以任务处理框架,尤其例如Executor应运而生。
任务处理框架(Executor)
用Executor框架实现 “每任务每线程”和顺序执行这2种调度策略的服务器
/**
* @since 1.5
* @author Doug Lea
*/
public interface Executor {
void execute(Runnable command);
}
这个简约的接口是Executor框架的基础,而Executor框架可用于处理异步任务,支持不同类型的任务执行策略,它解耦了任务提交和任务执行,包装Runnable来描述任务,还提供了任务的生命周期服务,当然还有线程池的实现,以及其他扩展功能。
接下来我们用Executor来实现“每任务每线程”这种调度策略的服务器:
/**
* TaskExecutionWebServer
* <p/>
* Web server using a thread pool
*
* @author Brian Goetz and Tim Peierls
*/
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
}
}
如果我们把上例中的Executor换成下面的ThreadPerTaskExecutor,这样就实现了“每任务每线程”这种调度策略的服务器。
/**
* ThreadPerTaskExecutor
* <p/>
* Executor that starts a new thread for each task
*
* @author Brian Goetz and Tim Peierls
*/
public class ThreadPerTaskExecutor implements Executor {
public void execute(Runnable r) {
new Thread(r).start();
};
}
下面的WithinThreadExecutor实现了顺序执行任务这种调度策略。
/**
* WithinThreadExecutor
* <p/>
* Executor that executes tasks synchronously in the calling thread
*
* @author Brian Goetz and Tim Peierls
*/
public class WithinThreadExecutor implements Executor {
public void execute(Runnable r) {
r.run();
};
}
执行策略
执行策略之所以可以定制,是因为Executor框架解耦了任务提交和任务执行。一个执行策略包括:
任务在什么(what)线程中执行。
任务以什么(what)顺序执行(FIFO,LIFO,优先级)。
可以有多少个(how many)任务并发。
可以有多少个(how many)任务进入等待执行队列。
如果系统过载,如果要放弃任务,应该挑选哪一个(which)任务。挑选的这个任务如何(how)让应用程序知道(抛异常,还是回调等)。
在一个任务执行前和结束后应该做些什么(what)。
所谓的最佳策略,取决于可用的计算资源和你对服务质量的需求。把任务提交和任务执行分离,让策略更好地为控制任务执行。
Executor中的线程池
线程池和任务队列紧密相关。任务队列持有所有等待执行的任务,而线程池里的线程会从任务队列中获取一个任务并执行,然后归还到线程池并等待下一个任务。
使用Executor有许多优势,具体见java编程实践的第6章6.2.3的叙述,这里列出创建Executor的一些方式,创建的Executor具体怎么使用也详见java编程实践的第6章6.2.3的叙述。
Executor中任务的生命周期管理
JVM只有在所有非守护线程全部终止后才退出,因此,无法正确地关闭Executor,JVM就无法退出。为了解决Executor的生命周期管理问题,ExecutorService接口提上了日程:
public interface ExecutorService extends Executor
这些方法具体的定义和内容详见java编程实践的第6章6.2.3的叙述,在此略过。下面举一个用到其中方法的例子:
/**
* LifecycleWebServer
* <p/>
* Web server with shutdown support
*
* @author Brian Goetz and Tim Peierls
*/
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;
}
}
延迟任务与周期任务-ScheduledExecutorService
使用Doug Lea 的ScheduledExecutorService 接口,延迟任务执行和周期性地执行。
public interface ScheduledExecutorService extends ExecutorService
不要使用Josh Bloch的Timer和TimerTask,他们有缺陷(详见6.2.2),如下例:程序运行1秒就结束了,说好的6秒呢,第一个TimerTask抛出异常后也会传染到第二个TimerTask。
/**
* OutOfTime
* <p/>
* Class illustrating confusing Timer behavior
*
* @author Brian Goetz and Tim Peierls
*/
public class OutOfTime {
public static void main(String[] args) throws Exception {
Timer timer = new Timer();
timer.schedule(new ThrowTask(), 1);
SECONDS.sleep(1);
timer.schedule(new ThrowTask(), 1);
SECONDS.sleep(5);
}
static class ThrowTask extends TimerTask {
public void run() {
throw new RuntimeException();
}
}
}
找出可利用的并行性-HTML渲染器
下面的程序串行地渲染页面元素:渲染文本-下载所有图片-渲染所有图片
/**
* SingleThreadRendere
* <p/>
* Rendering page elements sequentially
*
* @author Brian Goetz and Tim Peierls
*/
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);
}
使用Future等待所有图片下载完成后再渲染所有图片,让下载所有图片和渲染文本并行:
/**
* FutureRenderer
* <p/>
* Waiting for image download with \Future
*
* @author Brian Goetz and Tim Peierls
*/
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) {
// Re-assert the thread's interrupted status
Thread.currentThread().interrupt();
// We don't need the result, so cancel the task too
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);
}
分析上面的程序,使用2个线程分摊任务至多能将速度提升一倍,然而渲染文本的速度可能远远高于下载图像的速度,那么程序最终性能与串行执行的性能差别不大,代码还变复杂了,所以只有大量相互独立且同构的任务并发时,才能让性能真正提升。独立且同构的任务的分解变的异常重要。
然而,我们希望做的更好,每下载完一张图片,就渲染一张。
顺着使用Future的思路,我们可能会想到提交多个下载图片的task到Executor执行器,得到一组Future,然后串行遍历这一组Future一一获取结果再渲染,这样做会有什么性能问题吗?把下载时间最短任务排队头和把下载时间最长的排对头,似乎有些区别,把下载时间最长的排对头在获取其他任务结果的时候,如果其他任务已经下载完,则get方法不用在内部调用获取锁和释放锁的操作而是直接返回结果,把下载时间最长的排对头,可能得到其余的下载任务中直接返回结果的最多,从而在锁的获取与释放上节省一批时间;但是如何得知下载任务里哪一个下载时间最长,恐怕这不仅与文件大小有关还也要和当时的网络环境有关。所以串行遍历这一组Future的耗时和他们的排序有关,而如何排序显得格外棘手。最好的做法是把下载的情况通知给外边。然而,java并发实践给出另一种方案:使用concurrent包里的CompletionService,它实现了多线程中的生产者和消费者模式,使用submit提交任务生产产品,使用take和poll来异步地消费产品,其实现机制是使用Executor和BlockingQueue来组合实现,Executor把异步生产完成的产品异步地放入BlockingQueue(在FutureTask的done方法里放入),然后我们用CompletionService的get方法去BlockingQueue里获取。因此:
/**
* Renderer
* <p/>
* Using CompletionService to render page elements as they become available
*
* @author Brian Goetz and Tim Peierls
*/
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);
}
在指定的时间内获取广告信息(任务的获取有时长限制):
/**
* RenderWithTimeBudget
*
* Fetching an advertisement with a time budget
*
* @author Brian Goetz and Tim Peierls
*/
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());
// Render the page while waiting for the ad
Page page = renderPageBody();
Ad ad;
try {
// Only wait for the remaining time budget
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;
}
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();
}
}
}
可以为批量的任务设置超时时限,旅行门户网站订票信息显示,输入旅程的相关参数,就会显示来自各个航行运输公司的票务,路线信息,而为了及时响应,就不能等待一个获取时间特别长的信息后再统一显示,这时候就要统一设置这些任务的超时时长。
/**
* QuoteTask
* <p/>
* Requesting travel quotes under a time budget
*
* @author Brian Goetz and Tim Peierls
*/
public class TimeBudget {
private static ExecutorService exec = Executors.newCachedThreadPool();
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));
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 {
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;
}
}
class QuoteTask implements Callable<TravelQuote> {
private final TravelCompany company;
private final TravelInfo travelInfo;
public QuoteTask(TravelCompany company, TravelInfo travelInfo) {
this.company = company;
this.travelInfo = travelInfo;
}
TravelQuote getFailureQuote(Throwable t) {
return null;
}
TravelQuote getTimeoutQuote(CancellationException e) {
return null;
}
public TravelQuote call() throws Exception {
return company.solicitQuote(travelInfo);
}
}
interface TravelCompany {
TravelQuote solicitQuote(TravelInfo travelInfo) throws Exception;
}
interface TravelQuote {
}
interface TravelInfo {
}
处理线程的非正常结束
线程泄漏
线程泄漏,在线程池服务框架中,如果有一个线程因为RunTimeException未加处理而是默认地在控制台中输出栈追踪信息,并终止线程,这样的终止线程有可能是恶性的。 Timer的缺陷就是线程泄漏,可以参见前面的OutOfTimer类。
如下例,一个典型的线程池工作者线程结构,如果抛出的是一个未检查异常,首先会通知框架该线程已终止,然后框架可能会启用新的线程,或者不会启用新线程,或者当前有足够多的线程,或者怎么着等等。大家可以看看Worker的实现如下:
大家可以看看Worker的实现如下:
final void runWorker(Worker w) {
……
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
…….
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
……
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
线程未捕获的异常
未处理的异常可以通过设置线程的UncaughtExceptionHandler,从而把未检查的异常信息最终传递到UncaughtExceptionHandler,如果没有设置UncaughtExceptionHandler,则默认行为是将栈追踪信息输出到System.err。通常情况下,线程池里所有线程都转向同一个UncaughtExceptionHandler,并且该处理器至少将异常信息记录到日志文件中。
/**
* UEHLogger
* <p/>
* UncaughtExceptionHandler that logs the exception
*
* @author Brian Goetz and Tim Peierls
*/
public class UEHLogger implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
Logger logger = Logger.getAnonymousLogger();
logger.log(Level.SEVERE, "Thread terminated with exception: " + t.getName(), e);
}
}
在Executor框架中,可用ThreadFactory统一为线程设置UncaughtExceptionHandler。
关闭JVM
JVM既可以正常关闭,也可以强行关闭。正常关闭的触发方式有多种,包括:当最后一个“正常(非守护)”线程结束时,或者当调用了System.exit时,或者通过其他特定于平台的方法关闭时(例如发送了SIGINT信号或者键入Ctrl-C)。强行关闭包括:通过调用Runtime.halt或者在操作系统中“杀死”JVM进程(例如发送SIGKILL)。
关闭钩子
是指通过Runtime.addShutdownHook注册的但尚未开始的线程。
在正常关闭中,jvm首先调用所有已注册的关闭钩子,并不能保证关闭钩子的调用顺序。
在关闭应用程序时,如果有(守护或非守护)线程仍然在运行,那么这些线程将与关闭进程并发。jvm并不会停止或中断任何在关闭时仍然运行的应用程序线程。
当所有关闭钩子都执行结束时,如果runFinalizersOnExit为true,那么jvm将运行终结器,然后再停止。
当jvm最终结束时,这些线程将被强行结束。
如果关闭钩子或终结器没有执行完成,那么正常关闭进程挂起并且JVM必须强行关闭。
强行关闭JVM时,只是关闭JVM,不会运行关闭钩子。
关闭钩子不应该对应用程序的状态(如:其它服务是否已经关闭,或者 所有正常线程是否已经执行完成)或者对jvm的关闭原因做出假设,为的是关闭钩子应该尽快完成并退出,避免他们延迟jvm的结束时间。
注册的关闭钩子,未执行完的应用程序的线程,关闭进程他们是并发的。关闭钩子应该要线程安全,它们在访问共享数据时必须使用同步机制,并且避免发生死锁。看一个例子:
一个关闭日志服务的钩子。
关闭钩子是并发的,关闭日志服务可能会影响其他需要日志服务的钩子。为了避免避免这种依赖,或者说并发的情况,我们让所有服务使用同一个钩子,让所有服务的关闭操作在一个钩子内串行执行,可以确保关闭操作按照正确地顺序执行。
下面是测试类:
public class TestMyHook {
@SuppressWarnings("deprecation")
public static void main(String[] args) throws Exception {
new Finalizer();
//启用退出JVM时执行Finalizer
Runtime.runFinalizersOnExit(true);
MyHook hook1 = new MyHook("Hook1");
MyHook hook2 = new MyHook("Hook2");
MyHook hook3 = new MyHook("Hook3");
//注册关闭钩子
Runtime.getRuntime().addShutdownHook(hook1);
Runtime.getRuntime().addShutdownHook(hook2);
Runtime.getRuntime().addShutdownHook(hook3);
//移除关闭钩子
Runtime.getRuntime().removeShutdownHook(hook3);
//Main线程将在执行这句之后退出
System.out.println("Main Thread Ends.");
}
static class MyHook extends Thread {
private String name;
public MyHook (String name) {
this.name = name;
setName(name);
}
public void run() {
System.out.println(name + " Ends.");
}
//重写Finalizer,将在关闭钩子后调用
protected void finalize() throws Throwable {
System.out.println(name + " Finalize.");
}
}
static class Finalizer{
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("Finalizer Finalize.");
}
}
}
打印:
Main Thread Ends.
Hook2 Ends.
Hook1 Ends.
Hook3 Finalize.
Hook2 Finalize.
Hook1 Finalize.
Finalizer Finalize.
守护线程
普通线程与守护线程的差异仅在他们退出时发生的操作。当JVM停止时,所有守护线程被抛弃——既不会执行finally代码块,也不会回卷栈,而只是JVM直接退出。所以守护线程不能用来完成应用程序中的生命周期服务,这样可能会造成程序数据的不一致。
终结器
避免使用终结器。会带来同步、性能开销的麻烦。使用finally代码块和close方法替代。
转载笔记请注明出处。