Java并发:任务执行

任务执行

大多数并发应用程序都是围绕任务执行来构造的,任务通常是一些抽象的且离散的工作单元,通过把应用程序的工作分解到多个任务中,可以简化程序的组织结构,提供一种自然的事务边界优化错误恢复过程,提供一种自然的并行工作结构来提升并发性

在线程中执行任务

指明任务的边界,使得任务为一个独立的活动,不依赖其他任务的状态、结果、边界效应。独立性有助于实现并发,而不会导致任务间相互阻塞。

任务边界:在服务器应用程序中通常以单独的客户请求作为边界。

任务执行策略:

  • 串行执行
    • 即收到一个请求,服务器要处理完该请求才能继续accept下一个请求

1550825942991

  • 显式地为任务创建线程,为每一个服务请求创建一个线程
    • 并行处理,并且能同时接受多个请求
    • 任务处理代码必须线程安全
    • 任务处理过程从主线程中分离出来,使得主循环可以更快地重新等待下一个到来的连接。

1550825994345

无限制创建线程的不足

线程生命周期的开销、资源消耗。空闲线程会占用很多的内存。

无限制线程的稳定性拖垮系统。该限制与JVM相关,并且受到多个因素制约,包括JVM启动参数、Thread构造函数中请求的栈大小,以及底层系统对线程的限制等。如果破坏了可能导致OOM

Exeutor框架

详情参见JUC-Executor

任务取消

有时候我们希望提前结束任务或线程,或许是因为用户取消了操作,或应用程序需要被快速关闭。Java没有提供任何机制来安全地终止线程,即没有安全的抢占式方法停止线程。但它提供了中断,这是一种协作机制,能够使一个线程终止另一个线程的当前工作。

并且大多数时候我们希望任务可以安全地结束它们的任务,而不是强制停止。

任务取消

如果外部代码能在某个操作正常完成前将其置入“完成”状态,那么这个操作就可以称为可取消的,取消操作的原因有

  • 用户请求的取消,cancel
  • 限时活动,超时取消
  • 应用程序事件,当程序的不同任务在搜索,一个任务找到了解决方案,其他任务就取消
  • 错误,IO等错误
  • 关闭,关闭某个服务

一个可取消的任务必须拥有取消策略,定义取消操作的How、When、What。

中断

线程中断是一种协作机制,线程可以通过这种机制来通知另一个线程,告诉它在合适或者可能的情况下停止当前工作,并转而执行其他的工作。

实现

在每个线程中都有一个Boolean类型的中断状态,当中断线程时,这个线程的中断状态设置为true。

1
2
3
4
5
public class Thread{
public void interrupt();//中断目标线程
public boolean isInterrupt();//返回目标线程的中断状态
public static boolean interrupted();//清除当前线程的中断状态,并返回它的值。
}
  • interrupt:
    • 将线程的中断状态置位(中断状态由false变成true);
    • 让被中断的线程抛出InterruptedException异常。

调用interrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。

阻塞

对于阻塞库方法,例如Thread.sleep()Object.wait()都会去检查线程何时中断,并且在发现中断时提前返回。

在响应中断时会清除中断状态,并抛出InterruptedException,表示阻塞时由中断而提前结束的。但JVM不会保证阻塞方法检测到中断的速度。

阻塞与中断的关系是,如果在代码中实现中断,则由于阻塞将永远无法检测到中断信号。

当线程在非阻塞的状态下中断,它的中断状态将被设置,然后根据被取消的操作来检查中断状态以判断是否中断(在取消点)。即如果不触发InterruptedException那么中断状态将一直保持,直到明确清除中断状态。

因此如果任务代码能够响应中断,那么可以使用中断作为取消机制。

中断策略

中断策略规定线程如何解释某个中断请求——当发现中断请求时,应该做哪些工作,哪些工作单元对于中断来说是原子操作,以及以多快的速度来响应中断。

最合理的中断策略是某种形式的线程级取消操作或服务级取消操作:尽快退出,在必要时进行清理,通知某个所有者该线程已经退出。并可以建立其他中断策略,例如暂停服务或重新开始服务。

要区分线程与任务对中断的反应,可以是取消当前任务,也可以是关闭工作线程。

任务不会在其自己拥有的线程中执行,而是在某个服务(线程池)拥有的线程中执行。对于非线程所有者的代码(例如线程池,即任何在线程池实现以外的代码),应该小心保存中断状态,这样拥有线程的代码才能对中断做出响应。即大多数可阻塞的函数只是抛出InterruptException的原因,将中断信息传递给调用方线程。

由于每个线程拥有自己的中断策略,因此除非你知道中断对该线程的含义,否则就不应该中断这个线程。

响应中断

有两种策略可以处理InterruptedException

  • 传递异常,可能在执行某个特定于任务的清除操作后,从而使你的方法也成为可中断的阻塞方法
    • throw
  • 恢复中断状态,从而使调用栈中的上层代码能够对其进行处理
    • 再次调用interrupt来恢复中断线程,即不能屏蔽InterruptedException

只有实现了线程中断策略的代码才可以屏蔽中断请求,在常规的任务和库代码中都不应该屏蔽中断请求。

通过Future实现取消

处理不可中断的阻塞

许多可阻塞的方法都是通过提前返回或者抛出InterruptedException来响应中断请求的,而并非所有的可阻塞方法或者阻塞机制都能响应中断。如果一个线程由于执行同步的socket IO或等待内置锁而阻塞,则中断没有任何作用。

线程阻塞的原因:

  • 同步Socket IO。虽然read或write都不会响应中断,但是可以通过关闭套接字抛出SocketException
  • 同步IO。关闭一个正在InterruptibleChannel上等待的线程时,将抛出ClosedByInterruptException,并关闭链路。
  • Selector 异步IO。调用close或wake up会导致线程CloasedSelectorException
  • 获取某个锁。Lock类提供了lockInterruptibly方法,允许等待锁时可以中断。

采用newTaskFor封装非标准的取消

停止基于线程的服务

应用程序通常会创建拥有多个线程的服务,例如线程池,这些服务的生命周期通常比创建它们的方法的生命周期更长。如果应用程序结束,那么这些服务所拥有的线程也要结束。而且由于无法抢占式停止,因此需要它们自行结束。

正确的封装原则是:除非拥有某个线程,否则不能对该线程进行操控。线程有一个相应的所有者,即线程池,如果要中断这些线程,那么应该使用线程池。由于线程的所有权无法传递,因此服务应该提供生命周期方法来关闭他自己以及拥有的线程,因此当服务关闭时就会关闭所有的线程。

处理非正常的线程终止

JVM关闭

参考