java并发编程实践笔记三

目录

任务和线程

任务处理框架(Executor)

用Executor框架实现 “每任务每线程”和顺序执行这2种调度策略的服务器

执行策略

Executor中的线程池

Executor中任务的生命周期管理

延迟任务与周期任务-ScheduledExecutorService

找出可利用的并行性-HTML渲染器

处理线程的非正常结束

线程泄漏

线程未捕获的异常

关闭JVM

关闭钩子

守护线程

终结器


任务和线程

下面讨论服务器使用线程这种工具来调度任务的问题。

顺序地执行任务:

/**
 * SingleThreadWebServer
 * 

* 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
 * 

* 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
 * 

* 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
 * 

* 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 *

* 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的叙述。

java并发编程实践笔记三_第1张图片

 

Executor中任务的生命周期管理

JVM只有在所有非守护线程全部终止后才退出,因此,无法正确地关闭Executor,JVM就无法退出。为了解决Executor的生命周期管理问题,ExecutorService接口提上了日程:

public interface ExecutorService extends Executor

java并发编程实践笔记三_第2张图片

这些方法具体的定义和内容详见java编程实践的第6章6.2.3的叙述,在此略过。下面举一个用到其中方法的例子:

/**
 * LifecycleWebServer
 * 

* 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
 * 

* 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
 * 

* Rendering page elements sequentially * * @author Brian Goetz and Tim Peierls */ public abstract class SingleThreadRenderer { void renderPage(CharSequence source) { renderText(source); List imageData = new ArrayList(); 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 scanForImageInfo(CharSequence s); abstract void renderImage(ImageData i); }

使用Future等待所有图片下载完成后再渲染所有图片,让下载所有图片和渲染文本并行:

/**
 * FutureRenderer
 * 

* 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 imageInfos = scanForImageInfo(source); Callable> task = new Callable>() { public List call() { List result = new ArrayList(); for (ImageInfo imageInfo : imageInfos) result.add(imageInfo.downloadImage()); return result; } }; Future> future = executor.submit(task); renderText(source); try { List 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 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
 * 

* 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 info = scanForImageInfo(source); CompletionService completionService = new ExecutorCompletionService(executor); for (final ImageInfo imageInfo : info) completionService.submit(new Callable() { public ImageData call() { return imageInfo.downloadImage(); } }); renderText(source); try { for (int t = 0, n = info.size(); t < n; t++) { Future 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 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 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 {
        public Ad call() {
            return new Ad();
        }
    }

}

可以为批量的任务设置超时时限,旅行门户网站订票信息显示,输入旅程的相关参数,就会显示来自各个航行运输公司的票务,路线信息,而为了及时响应,就不能等待一个获取时间特别长的信息后再统一显示,这时候就要统一设置这些任务的超时时长。

/**
 * QuoteTask
 * 

* Requesting travel quotes under a time budget * * @author Brian Goetz and Tim Peierls */ public class TimeBudget { private static ExecutorService exec = Executors.newCachedThreadPool(); public List getRankedTravelQuotes(TravelInfo travelInfo, Set companies, Comparator ranking, long time, TimeUnit unit) throws InterruptedException { List tasks = new ArrayList(); for (TravelCompany company : companies) tasks.add(new QuoteTask(company, travelInfo)); List> futures = exec.invokeAll(tasks, time, unit); List quotes = new ArrayList(tasks.size()); Iterator taskIter = tasks.iterator(); for (Future 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 { 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的实现如下:

java并发编程实践笔记三_第3张图片

大家可以看看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
 * 

* 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的结束时间。

注册的关闭钩子,未执行完的应用程序的线程,关闭进程他们是并发的。关闭钩子应该要线程安全,它们在访问共享数据时必须使用同步机制,并且避免发生死锁。看一个例子:

一个关闭日志服务的钩子。

java并发编程实践笔记三_第4张图片

关闭钩子是并发的,关闭日志服务可能会影响其他需要日志服务的钩子。为了避免避免这种依赖,或者说并发的情况,我们让所有服务使用同一个钩子,让所有服务的关闭操作在一个钩子内串行执行,可以确保关闭操作按照正确地顺序执行。

下面是测试类:

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方法替代。

转载笔记请注明出处。

你可能感兴趣的:(并发编程)