读书笔记: Java并发编程实战(3)

本文深入探讨了并发编程的核心概念,包括任务与线程的关系、线程池的使用、Executor框架的功能,以及如何利用并行性提高程序效率。文章还介绍了Callable与Future、FutureTask的作用,CompletionService的使用方法,以及任务取消与关闭的策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

3 任务与线程

本章主要介绍任务的并发处理。

3.1 在线程中执行任务

串行执行任务
所有任务在单个线程中执行,程序简单,安全性也高。但无法提供高吞吐率和快速响应性。

显式为任务创建线程
通过创建新的线程为任务提供服务,将任务处理逻辑从主线程中分离出来,多个任务可以并行处理,提高了吞吐量和响应速度。

当短时间内要处理的任务较多时,为每一个任务创建一个新的线程并不是一个好的做法。因为线程的创建和销毁都是需要开销的,大量的线程在竞争CPU资源时还将产生其他的性能开销。另外,可创建的线程数量是有限制的,并不能无限创建。

3.2 Executor框架

任务是一组逻辑单元,而线程是使任务异步执行的机制。

Executor是异步任务处理框架的一个基础接口,线程池是它的一个重要实现部分。
Executor是基于生产者-消费者模式设计的,提交任务的操作相当于生产者,执行任务的线程则相当于消费者。通过将任务的提交与执行解耦开来,无须太大困难就可以为某种类型的任务指定和修改执行策略。

Executor只有一个方法,即execute方法,用于执行任务。ExecutorService是Executor的扩展接口,而线程池ThreadPoolExecutor即是ExecutorService的实现。博主以前的blog中已经介绍过线程池,这里不再赘述。

延迟任务与周期任务可通过Timer或ScheduledThreadPoolExecutor来实现。当然Timer功能较弱,完全可以用quartz代替。ScheduledThreadPoolExecutor只支持基于相对时间的调度。

3.3 找出可利用的并行性

当多个任务之间无依赖关系时,这些任务是可以并行处理的。

比如一段代码需要调用三个外部系统,获得全部调用返回结果之后再进行其他处理,如果串行执行的话所花费的时间就是三次调用之和,而并行处理的话那么就只需要花费耗时最久的那个调用时间。

并行处理的任务需要能返回结果。Executor框架使用Runnable作为其基本的任务表示形式,但Runnable不能返回一个值或抛出一个受检查的异常。

3.3.1 Callable与Future、FutureTask

在上一篇的同步工具类里介绍FutureTask时提到过Callable和Future。

与Runnable相比,Callable的接口实例可以实现有返回值的线程任务,其返回值就放在Future中。

Future表示一个任务的生命周期,提供了相应的方法来判断任务是否已经完成或取消,以及获取任务的结果和取消任务等。

在Future中,get方法用于获取任务结果,其行为取决于任务的状态(未开始、正在运行、已完成)。如果任务正常执行完成,那么get会立即返回结果;如果任务没有完成,那么get将阻塞并直到任务完成。

当任务执行过程中抛出异常时,get会将该异常封装为ExecutionException并重新抛出,通过getCause可以获取初始异常。当任务取消时,get会抛出CancellationException。当任务中断时抛出InterruptedException。因此通过get的返回值,可以判断任务的状态。

FutureTask是一个异步任务类,实现了RunnableFuture接口,而Runnable接口继承了Runnable接口和Future接口。因此FutureTask既可以作为任务提交,又能返回任务执行的结果。当需要返回任务执行结果时,需要对Callable对象进行包装,不需要返回任务结果时则可以对Runnable对象进行包装。

3.3.2 CompletionService

如果向Executor提交了一组计算任务,并且在计算完成后获得结果,那么可以保留与每个任务关联的Future,然后通过轮询的方式来判断任务是否完成。

这种实现在上一篇中已经举过栗子,通过两个while循环来轮询遍历,按执行顺序先后输出最终结果。随后提出通过FutureTask来对Callable对象进行包装,然后覆写FutureTask的done方法来实现任务完成的回调,这样就不用轮询遍历了。

事实上还有另一种方式,即通过ComletionService来实现:

public static void main(String[] args) throws Exception {
    // 通过5个线程执行任务,并输出最终结果
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    CompletionService<String> completionService = new ExecutorCompletionService(executorService);
    for (int i = 0; i < 5; i++) {
        completionService.submit(new Callable<String>() {

            @Override
            public String call() throws Exception {
                TimeUnit.SECONDS.sleep(new Random().nextInt(2));
                return Thread.currentThread().getName();
            }
        });
    }
    for (int i = 0; i < 5; i++) {
        Future<String> future = completionService.take();
        try {
            System.out.println(future.get());
        } catch (ExecutionException ee) {
            logger.error("任务执行异常,{}", ee.getCause());
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
        }
    }
    executorService.shutdown();
}

通过CompletionService提交任务,CompletionService会将任务委托给ExecutorService来执行,并根据任务执行完成的顺序自动将结果放到BlockingQueue中。开发者只需要通过take方法取出任务Future获得顺序执行的结果。

ExecutorCompltionService的实现是相当简单的,就是在构造函数中创建一个BlockingQueue来保存任务结果。当任务执行完成时,调用FutureTask中的done方法,将结果放入BlockingQueue中。

此种方式不需要轮询遍历,也不需要覆写FutureTask的任务完成回调方法。事实上其内部实现就是结果保存和任务完成回调的结合。

3.4 任务取消与关闭

待续。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值