Java并发编程实战————Executor框架与任务执行

引言

本篇博客介绍通过“执行任务”的机制来设计应用程序时需要掌握的一些知识。所有的内容均提炼自《Java并发编程实战》中第六章的内容。

大多数并发应用程序都是围绕“任务执行”来构造的:任务通常是一些抽象的且离散的工作单元。

当围绕“任务执行”来设计应用程序结构时,第一步,就是要找出清晰的任务边界。在理想情况下,各个任务之间是相互独立的:任务并不依赖于其他任务的状态结果边界效应

大多数服务器应用程序提供了一种自然的任务边界选择方式:以独立的客户请求为边界。

不好的例子:串行与“为每个任务创建线程”

在书中6.1节,介绍了由最简单的串行执行任务到为每个任务创建一个线程这两种执行任务的方式。应该说这两种方式都是不可取的。

这一节主要是为了引出下一节介绍的“任务执行框架”。

其中“串行执行任务”的缺点是在一般的服务器应用程序中,无法提高吞吐率或快速响应性。

而“为每个任务创建线程”的方式的问题在于可能导致:高性能开销、高资源消耗、影响稳定性

重点】在工作或面试中也会遇到这个极富针对性的问题,即大量创建线程会存在哪些问题?

1、高性能开销:创建和销毁都需要一定的代价,创建过程需要时间,延迟处理请求,也需要jvm和操作系统提供一些辅助操作。

2、高资源消耗:活跃的线程会消耗系统资源,尤其是内存。当可运行的线程数量多余可用处理器的数量,那么会有大量空闲的线程占用内存,不仅给垃圾回收带来压力,在竞争CPU的时候还将产生额外的性能开销。

3、影响稳定性:大量线程占用内存,内存不足,导致可能抛出OutOfMemoryError,系统崩溃。

线程数量的限制

书中在这里简单引出一个概念:稳定性。

根据前后文的联系,这里具体指的是:应用程序不会因为线程过多而抛出OutOfMemoryError异常

为了达到这种稳定性,在可创建线程数量上存在一个限制。这个限制受平台以及多个因素影响,包括JVM启动参数、Thread构造函数中请求的栈大小、底层操作系统对线程的限制等。例如,在32位机器上,其中一个主要的限制因素是线程栈的地址空间。每个线程都维护两个执行栈,一个用于Java代码,另一个用于原生代码。

通常,JVM在默认情况下会生成一个复合栈,大小约0.5M~1M(这个值可以通过JVM标志 -Xss或通过Thread的构造函数来修改),那么:线程数量 ≈ 2^32(bit) / 0.5(MB) ≈几千或几万

因此,在一定范围内,增加线程可以提高系统的吞吐率,但如果超出这个范围,再创建更多的线程只会降低程序的执行速度。

Executor接口

public interface Executor {  void execute(Runnable command);  
}  

Executor是一个非常简单的接口,只有一个execute(Runnable) 方法,它是其他的灵活且强大的异步任务框架的基础。通过这种方式,用Runnable来表示任务,可以将任务的提交过程与执行过程解耦

Executor本身就是基于生产者消费者,提交任务相当于生产者,执行任务相当于消费者,因此,如果要在程序中实现一个生产者-消费者的设计,那么最简单的方式通常就是使用Executor。

什么是执行策略?

执行策略,定义了任务执行的“what、where、when、how”等方面,主要是描述根据不同的资源而选择不同的执行方式,一个最优执行策略应当是与硬件资源最匹配的。

线程池

先来看一下四种常用线程池的创建:

ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(10);  
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();  
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(10);  
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();

其中:ExecutorService extends Executor,ScheduledExecutorService extends ExecutorService 。 

1、newFixedThreadPool(int) :创建一个定额线程池,每提交一个任务创建一个线程,达到数量限制后不再增加,这时线程池的规模将不再变化(如果某个线程由于发生了未预期的异常而结束,那么线程池会补充一个新的线程)

2、NewCachedThreadPool() : 创建一个可缓存的线程池,线程池的规模不存在任何限制,当线程多余任务时,回收空闲线程;当任务增加时,创建新线程。

3、NewSingleThreadExecutor:单线程的Executor,如果这个线程异常结束,会创建另一个线程来替代。NewSingleThreadExecutor能确保依照任务在队列中的顺序串行执行(例如FIFO、LIFO、优先级)。

4、NewScheduleThreadPool:创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。

Executor的生命周期

 JVM只有在所有(非守护)线程全部终止后才会退出,无法正确地关闭Executor,JVM将无法结束。

Executor以异步的方式来执行任务,导致了提交任务的状态不是立即可见的,即有些任务可能已经完成,有些可能正在执行,还有些可能正在队列中等待执行。

ExecutorSevice接口就是为了解决执行服务的生命周期问题,扩展了Executor接口。它添加了一些用于声明周期管理的方法(同时还有一些用于任务提交的便利方法):

public interface ExecutorService extends Executor {  void shutdown();  List<Runnable> shutdownNow();  boolean isShutdown();  boolean isTerminated();  boolean awaitTermination(long timeout, TimeUnit unit)  throws InterruptedException;  // ......其他用于任务提交的便利方法  
}  

这五个方法是声明周期管理的方法,其余的都是与任务提交相关的方法,比如,可以提交比较大的集合Callable对象的方法:

invokeAll(Collection<? extends Callable<T>> tasks)

重点】ExecutorService的三种状态:运行、关闭、已终止 。

ExecutorService在初始创建时处于运行状态。shutdown()方法将执行平缓的关闭过程:不再接受新的任务,同时等待已经提交的任务执行完成——包括那些还未开始执行的任务。shutdownNow()方法将执行粗暴的关闭方式:它将尝试取消所有运行中的任务,并且不再启动队列中尚未开始执行的任务。

延迟任务与周期任务

Timer类负责管理延迟任务以及周期任务,但它本身存在缺陷,因此通常要用ScheduleThreadPoolExecutor的构造函数或newScheduleThreadPool工厂方法来创建该类对象。

Timer的缺陷在于,Timer在执行所有定时任务时只会创建一个线程。如果某个任务的执行时间过长,那么将破坏其他TimerTask的定时精确性。

Timer还有一个问题就是,Timer线程不会捕获异常,当TimerTask抛出未检查异常时将终止定时线程。Timer也不会恢复线程的执行,而是会错误地任务整个Timer都被取消了。这就造成:已经被调度但尚未执行的TimerTask将不会再执行,新的任务也不会被调度。称之为“线程泄漏”。

【重点】生命周期小结

Runnable和Callable等任务的生命周期:创建、提交、开始、完成、取消。

Future表示的就是一个任务的生命周期。

Thread的生命周期:创建、就绪、运行、阻塞、死亡(或结束)。

ExecutorService的生命周期(因为它继承自Executor,因此也是Executor的生命周期):创建、运行、关闭、已终止。

Callable与Future

callable

Runnable有一个局限性是没有返回值,也没办法抛出受检异常。对于某些异步获得结果的任务无法胜任,Callable应运而生。

它是Runnable的升级版,既可以使用Callable<Void>来达到Runnable一样的效果,同时也可以使用Callable<T> 来指定返回结果。

创建Callable的方式有两种:构造函数、静态的封装方法。

Callable<String> callableTask = new Callable<String>() {  @Override  public String call() throws Exception {  return "this is a callable task....";  }  
}; 

Java 8 style:

Callable<String> callableTask = () -> {  return "this is a callable task....";  
};

静态方法:Executors.callable(Runnable task, T result):

Callable<String> call = Executors.callable(() -> {System.out.println("this is a runnable task...");
}, "done!");

Future

future表示一个任务的生命周期。主要提供了一些方法用于判断任务处于哪个阶段,还可以获取任务的结果甚至是取消任务。它本身还有一层隐含意义是,任务的生命周期只能前进,不能后退,当一个任务处于“完成”状态,就永远停留在“完成”状态上。这一点和ExecutorService的生命周期一样。

Future接口:

public interface Future<V> {  boolean cancel(boolean mayInterruptIfRunning);  boolean isCancelled();  boolean isDone();  V get() throws InterruptedException, ExecutionException;  V get(long timeout, TimeUnit unit)  throws InterruptedException, ExecutionException, TimeoutException;  
}

创建Future的方式通常是使用ExecutorService的submit()方法获取返回值。如果想通过构造器的方式显式地创建一个任务的生命周期管理对象,可以使用FutureTask。

FutureTask<String> runnFutureTask = new FutureTask<String>(runnable, "done!");  
FutureTask<String> callFutureTask = new FutureTask<>(callable);

FutureTask类实现了Runnable和Future两个接口。

说明:FutureTask是Java 5加入的类,Java 6又为它补充了一个新的RunnableFuture接口,Runnable接口和Future接口被提升到了RunnableFuture接口上,这更像是一种重构手段,我个人认为在实际开发中用途可能不及直接使用FutureTask

由于FutureTask实现了Runnable接口,因此可以将它提交给Executor来执行,或者直接调用它的run方法。

是的,FutureTask的run()方法可以直接执行任务,而不需要什么start。

Future.get

get()方法的行为取决于任务的状态(尚未开始、正在运行、已完成)如果任务已经完成,那么get会立即返回或抛出一个Exception;如果任务没有完成,那么get将阻塞直到任务完成。如果任务抛出异常,那么get将该异常封装成ExecutionException并重新抛出,可以通过getCause来进一步获得被封装的初始异常。如果任务被取消,那么get将抛出CancellationException。

异构任务并行化存在的局限

A与B两个完全不同的任务通过并行方式可以实现小幅度的性能提升,但是如果想大幅度的提升存在一定的困难。因此,得出一个结论是,只有当大量相互独立且同构的任务可以并发进行处理时,才能体现出真正的性能提升。

CompletionService与它的子类ExecutorCompletionService

CompletionService是Executor与BlockingQueue的融合。

回顾一下BlockingQueue的一些特性:

BlockingQueue接口是Queue的子接口,有两个最主要的实现,LinkedBlockingQueue(无界队列)和ArrayBlockingQueue(有界队列)。take()或poll()方法都是BlockingQueue的取头元素的方法,唯一不同的是当没有可用的头元素时,take会无限期等待(阻塞),poll可以设置一个超时时间,一旦超时,将返回null。

CompletionService是在任务执行的功能上加入了队列的特性,很明显是用于处理一批允许有返回值的任务。

用法:创建一个CompletionService(ExecutorCompletionService对象ExecutorCompletionService的构造器允许我们传入一个ExecutorService(用于采取不同的执行策略)和一个BlockingQueue(该参数可选,默认LinkedBlockingQueue)然后可以将一组Callable任务提交给CompletionService来执行,然后使用类似队列操作的take或poll方法来获取已完成的结果,这些结果会在完成时被封装为Future。

扩展ExecutorCompletionService的实现很简单。首先通过构造函数创建一个BlockingQueue来保存计算结果,然后当计算完成时,调用FutureTask的done方法,放入队列。展开:当提交某个任务时,该任务将首先包装为一个QueueingFuture,这是FutureTask【回顾:FutureTask实现了Future、Runnable】的一个子类,QueueingFuture改写了FutureTask的done方法——将结果放入BlockingQueue中。take和poll方法委托给BlockingQueue方法,这些方法会在得到结果之前阻塞。

为任务设置时限

有时候,如果某个任务无法在指定时间内完成,那么将不再需要它的结果,此时可以放弃这个任务。Future.get中支持这种需求:当结果可用时,它将立即返回,如果在指定时限内没有计算出结果,那么抛出TimeoutException。

在使用时限任务时需要注意,当这些任务超市后应该立即停止,从而避免为继续计算一个不再使用的结果而浪费计算资源。

【使用Future.get为单个任务设置时限,如果希望对一组任务设置计算时限,比如前面介绍的CompletionService,那么可以使用poll方法来设置执行时间】

invokeAll方法

ExecutorServie接口中有两个重载的invokeAll方法:

<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)  throws InterruptedException;  
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,  long timeout, TimeUnit unit)  throws InterruptedException;

invokeAll方法支持将多个任务提交到一个ExecutorService并获得结果。invokeAll方法的参数为一组任务,并返回一组Future。invokeAll按照任务集合中迭代器的顺序将所有的Future添加到返回的集合中,从而使调用者能够将各个Future与其表示的Callable关联起来。

当所有任务都执行完毕时,或者调用线程被中断时,又或者超过指定时限时,invokeAll都会返回。当超过指定时限,任何还未完成的任务都会取消。当invokeAll返回后,每个任务要么正常完成,要么被取消,而客户端代码可以调用get或isCancelled来判断究竟是何种情况。

第六章小结

通过围绕任务执行来设计应用程序,可以简化开发过程,并有助于实现并发。

Executor框架将任务提交与执行策略解耦开来,同时还支持多种不同类型的执行策略。当需要创建线程来执行任务时,可以考虑使用Executor。

要想在将应用程序分解为不同的任务时获得最大的好处,必须定义清晰的任务边界。某些应用程序中存在着比较明显的任务边界,而在其他一些程序中则需要进一步分析才能揭示出粒度更细的并行性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/558826.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一篇博客读懂设计模式之---工厂模式

设计模式之—工厂模式 工厂模式&#xff1a; 创建过程&#xff1a; 创建Shape接口 public interface Shape {void draw(); }创建实现类&#xff1a; public class Circle implements Shape {Overridepublic void draw() {System.out.println("this is a circle!"…

强健程序员体魄————减脂原理

一、基本概念 运动消耗&#xff1a;任何肌肉活动。 热量摄入&#xff1a;指人体从外界食物中获得的总的能量。 基础代谢&#xff1a;人体维持生命的所有器官所需要的最低能量需要。与人的年龄、性别、体型&#xff08;肌肉量&#xff09;、气温有关。简单说&#xff0c;一个…

一篇博客读懂设计模式之-----策略模式

设计模式之策略模式 在策略模式中&#xff0c;我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的对象 定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。 主要解决&#xff1a;在有多种算法相似的情况下&#xff0c;使用 if…else 所带来的复杂和…

Java并发编程实战————恢复中断

中断是一种协作机制&#xff0c;一个线程不能强制其他线程停止正在执行的操作而去执行其他操作。 什么是中断状态&#xff1f; 线程类有一个描述自身是否被中断了的boolean类型的状态&#xff0c;可以通过调用 .isInterrupted() 方法来查看。官方解释如下&#xff1a; 简单来…

一篇博客读懂设计模式之---模板方法模式

设计模式之模板方法模式&#xff1a; 定义一个操作中的算法的骨架&#xff0c;而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 简而言之就是&#xff1a;父类定义了骨架&#xff08;调用哪些方法及其顺序&#xff09;…

Spring Boot————单元测试

引言 由于spring boot在启动时通常会先行启动一些内置的组件&#xff0c;比如tomcat。因此&#xff0c;spring boot的测试类一般需要加一些简单的注解。 一、添加依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-…

一篇读懂--mybatis的缓存

一篇读懂–mybatis的缓存 MyBatis的缓存指的是缓存查询结果&#xff0c;当以后使用相同的sql语句、传入相同的参数进行查询时&#xff0c;可直接从mybatis本地缓存中获取查询结果&#xff0c;而不必查询数据库。 mybatis的缓存包括一级缓存、二级缓存&#xff0c;一级缓存默认…

Spring Boot————BeanCreationNotAllowedException异常分析

引言 在对数据库进行新增记录的JUnit测试时&#xff0c;抛出一个BeanCreationNotAllowedException异常&#xff1a; 异常分析与解决 异常信息太长&#xff0c;图片截不下&#xff0c;粘贴来看&#xff1a; Exception in thread "pool-2-thread-1" org.springframew…

一篇博客读懂设计模式之---委派模式

一篇博客读懂设计模式之—委派模式 委派模式可能大家听起来不太熟悉&#xff0c;但是在代码开发的时候却很好用&#xff0c;下面从几个方面来介绍一下 what&#xff1a;是什么&#xff1f; 委派模式&#xff1a;顾名思义&#xff0c;委托其他对象或者实例来帮我们完成任务&am…

XML模板解析————Dom4j解析xml案例分析

引言 目前项目中包含大量的xml模板文件&#xff0c;现就xml模板的数据解析、提取、及部分常用方法做简单的应用和总结。 一、XML文件转为Document对象 通过SAXReader对象的read方法&#xff0c;读取Document对象。 SAXReader reader new SAXReader(); Document document …

教你如何一篇博客读懂设计模式之—--工厂模式

一篇博客读懂设计模式之—工厂模式 工厂模式在我们日常开发的时候经常用到&#xff0c;相信大家都有了一定的了解&#xff0c;工厂模式是一种创建对象的设计模式&#xff0c;它提供一种创建对象的最佳方式。 主要过程是&#xff1a; 定义一个创建对象的接口&#xff0c;让其子…

Jackson快速入门

引言 上一篇博客《XML模板解析————Dom4j解析xml案例分析》简单讲解了关于xml模板的解析&#xff0c;使用到了dom4j&#xff0c;这篇文章其实算是个姊妹篇&#xff0c;都是对于目前工作中的一些任务&#xff0c;如xml、json相互解析所涉及到的知识。 但是相对于xml而言&am…

一篇文章读懂MySQL的各种联合查询

一篇文章读懂MySQL的各种联合查询 联合查询是指将两个或两个以上的表的数据根据一定的条件合并在一起! 联合查询主要有以下几种方式&#xff1a; 全连接&#xff1a;将一张表的数据与另外一张表的数据彼此交叉联合查询出来 举例如下&#xff1a; 先建两张表&#xff1a; CR…

Class.forName()、Class.class、getClass() 区别

问&#xff1a;简单谈谈你对 Java 中 Class.forName()、Class.class、getClass() 三者的理解&#xff1f; Class.class 的形式会使 JVM 将使用类装载器将类装入内存&#xff08;前提是类还没有装入内存&#xff09;&#xff0c;不做类的初始化工作&#xff0c;返回 Class 对象…

教你如何一篇博客读懂设计模式之—--原型模式

教你如何一篇博客读懂设计模式之----原型模式 what&#xff1a;是什么 原型模式&#xff1a; 用于创建重复的对象&#xff0c;既不用一个属性一个属性去set和get&#xff0c;又不影响性能&#xff0c;原型模式产生的对象和原有的对象不是同一个实例&#xff0c;他们的地址也…

Java反射————Method根据方法名称字符串调用方法

引言 之前浏览廖雪峰老师的个人博客网站&#xff0c;无意间发现了关于在Java8中获取参数的方法&#xff0c;随手一转《Java 8中获取参数名称》&#xff0c;没想到今天遇到一个功能&#xff0c;非常符合这种反射调用的使用场景。回看了这篇之前转载的文章&#xff0c;然后根据自…

一篇文章看懂@Scheduled定时器/@Async/CompletableFuture

一篇文章看懂Scheduled定时器/Async/CompletableFuture Scheduled注解解析&#xff1a; 1.cron&#xff1a;最重要的一个参数 cron表达式[秒] [分] [小时] [日] [月] [周] [年]&#xff08;[年]可省略&#xff09; 简单了解一下&#xff0c;网上有现成的工具 示例&#xff…

一篇搞懂HTTP协议

本文转自 &#xff1a;flyhero 码上实战《一个HTTP打趴80%面试者》 HTTP协议简介 HTTP&#xff08;超文本传输协议&#xff09;是应用层上的一种客户端/服务端模型的通信协议,它由请求和响应构成&#xff0c;且是无状态的。&#xff08;暂不介绍HTTP2&#xff09; 协议&…

注册gmail邮件,遇到“此电话号码无法用于进行验证”该怎么办

注册gmail邮件&#xff0c;遇到“此电话号码无法用于进行验证”该怎么办&#xff1f; 跟浏览器语言的设置有关&#xff0c;将语言改为英文即可&#xff0c;亲测有效&#xff01;

Jackson高级操作————节点树

引言 继《Jackson快速入门》基础篇之后的树模型相关操作。 节点树模型 ObjectMapper构建JsonNode节点树&#xff0c;类似于DOM解析器的XML。 Testpublic void testJsonTree() throws JsonProcessingException, IOException {String jsonString "{\"name\":\…