java并发编程实践笔记三

目录

任务和线程

任务处理框架(Executor)

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

执行策略

Executor中的线程池

Executor中任务的生命周期管理

延迟任务与周期任务-ScheduledExecutorService

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

处理线程的非正常结束

线程泄漏

线程未捕获的异常

关闭JVM

关闭钩子

守护线程

终结器


任务和线程

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

顺序地执行任务:

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

转载笔记请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值