Java并发编程学习笔记9

96 阅读4分钟

任务执行

串行的执行任务

串行处理很少能提供良好的吞吐量或良好的响应能力。

显式的为任务创建线程

一种更具响应性的方法是创建一个新线程来为每个请求提供服务。

在轻度到中度负载下,每个任务一个线程的方法是对顺序执行的改进。只要请求到达率不超过服务器处理请求的能力,这种方法就能提供更好的响应能力和吞吐量。

无限制创建线程的不足

  • 线程生命周期的开销非常高

为每个请求创建新线程将消耗大量的计算资源

  • 资源消耗

如果可用线程的数量多余可用处理器的数量,将会出现闲置线程,大量闲置线程会占用许多内存,给垃圾回收带来压力。如果有足够多的线程让cpu保持忙碌,那么再创建更多的线程反而会降低性能。

  • 稳定性

可创建线程的数量存在一个上限。

Executor

Executor 可能是一个简单的接口,但它构成了支持各种任务执行策略的灵活而强大的异步任务执行框架的基础。它提供了一种将任务提交与任务执行分离的标准方法,使用 Runnable 描述任务。 Executor 实现还提供生命周期支持,用于添加统计信息收集、应用程序管理和性能监控等机制。

执行器基于生产者-消费者模式,其中提交任务的活动是生产者(生产要完成的工作单元),执行任务的线程是消费者(消费这些工作单元)。使用执行器通常是在您的应用程序中实现生产者-消费者设计的最简单途径。

执行策略

  • 任务会在什么线程执行?  
  • 任务应该以什么顺序执行(先进先出、后进先出、优先顺序)? 
  • 有多少任务可以同时执行? 
  • 有多少任务可以排队等待执行?
  • 如果一个任务因为系统过载而不得不被拒绝,应该选择哪个任务作为受害者,应该如何通知应用程序?
  • 在执行任务之前或之后应该采取什么行动?

每当您看到以下形式的代码:new Thread(runnable).start() 并且您认为您可能在某个时候想要更灵活的执行策略时,请认真考虑使用 Executor 替换它。

线程池

在线程池中执行任务比线程任务方法有很多优势。重用现有线程而不是创建新线程可以在多个请求中分摊线程创建和拆卸成本。作为一个额外的好处,由于工作线程通常在请求到达时已经存在,与线程创建相关的延迟不会延迟任务执行,从而提高响应能力。通过适当调整线程池的大小,您可以拥有足够的线程来保持处理器忙碌,同时又不会因为线程数量过多而导致应用程序内存不足或因线程之间的资源竞争而崩溃。

Executor 生命周期

ExecutorService 隐含的生命周期具有三种状态——运行、关闭和终止。 ExecutorServices 最初是在运行状态下创建的。 shutdown 方法启动正常关闭:不接受新任务,但允许完成之前提交的任务——包括那些尚未开始执行的任务。 shutdownNow 方法启动突然关闭:它尝试取消未完成的任务并且不启动任何排队但未开始的任务。

延迟和周期性任务

Timer 有一些缺点,应该将 ScheduledThreadPoolExecutor 视为它的替代品。

定时器只创建一个线程来执行定时器任务。如果计时器任务运行时间过长,其他 TimerTask 的计时精度可能会受到影响。如果一个循环 TimerTask 被安排为每 10 毫秒运行一次,而另一个 TimerTask 需要 40 毫秒运行,则循环任务(取决于它是按固定速率还是固定延迟调度)在长时间运行后快速连续调用四次-running 任务完成,或完全“错过”四次调用。计划线程池通过让您提供多个线程来执行延迟和定期任务来解决此限制。

Timer 的另一个问题是,如果 TimerTask 抛出未经检查的异常,它会表现不佳。定时器线程不捕获异常,因此从 TimerTask 抛出的未经检查的异常会终止定时器线程。在这种情况下,定时器也不会复活线程;相反,它错误地假设整个 Timer 都被取消了。在这种情况下,已经安排但尚未执行的 TimerTasks 永远不会运行,并且无法安排新任务。 (这个问题,称为“线程泄漏”)