任务执行
大多数并发应用程序都是围绕任务执行来构造的,任务通常是一些抽象的且离散的工作单元,通过把应用程序的工作分解到多个任务中,可以简化程序的组织结构,提供一种自然的事务边界优化错误恢复过程,提供一种自然的并行工作结构来提升并发性
在线程中执行任务
指明任务的边界,使得任务为一个独立的活动,不依赖其他任务的状态、结果、边界效应。独立性有助于实现并发,而不会导致任务间相互阻塞。
任务边界:在服务器应用程序中通常以单独的客户请求作为边界。
任务执行策略:
- 串行执行
- 即收到一个请求,服务器要处理完该请求才能继续accept下一个请求
- 显式地为任务创建线程,为每一个服务请求创建一个线程
- 并行处理,并且能同时接受多个请求
- 任务处理代码必须线程安全
- 任务处理过程从主线程中分离出来,使得主循环可以更快地重新等待下一个到来的连接。
无限制创建线程的不足
线程生命周期的开销、资源消耗。空闲线程会占用很多的内存。
无限制线程的稳定性拖垮系统。该限制与JVM相关,并且受到多个因素制约,包括JVM启动参数、Thread构造函数中请求的栈大小,以及底层系统对线程的限制等。如果破坏了可能导致OOM
Exeutor框架
详情参见JUC-Executor
任务取消
有时候我们希望提前结束任务或线程,或许是因为用户取消了操作,或应用程序需要被快速关闭。Java没有提供任何机制来安全地终止线程,即没有安全的抢占式方法停止线程。但它提供了中断,这是一种协作机制,能够使一个线程终止另一个线程的当前工作。
并且大多数时候我们希望任务可以安全地结束它们的任务,而不是强制停止。
任务取消
如果外部代码能在某个操作正常完成前将其置入“完成”状态,那么这个操作就可以称为可取消的,取消操作的原因有
- 用户请求的取消,cancel
- 限时活动,超时取消
- 应用程序事件,当程序的不同任务在搜索,一个任务找到了解决方案,其他任务就取消
- 错误,IO等错误
- 关闭,关闭某个服务
一个可取消的任务必须拥有取消策略,定义取消操作的How、When、What。
中断
线程中断是一种协作机制,线程可以通过这种机制来通知另一个线程,告诉它在合适或者可能的情况下停止当前工作,并转而执行其他的工作。
实现
在每个线程中都有一个Boolean类型的中断状态,当中断线程时,这个线程的中断状态设置为true。
1 | public class Thread{ |
- interrupt:
- 将线程的中断状态置位(中断状态由false变成true);
- 让被中断的线程抛出InterruptedException异常。
调用interrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。
阻塞
对于阻塞库方法,例如Thread.sleep()
、Object.wait()
都会去检查线程何时中断,并且在发现中断时提前返回。
在响应中断时会清除中断状态,并抛出InterruptedException
,表示阻塞时由中断而提前结束的。但JVM不会保证阻塞方法检测到中断的速度。
阻塞与中断的关系是,如果在代码中实现中断,则由于阻塞将永远无法检测到中断信号。
当线程在非阻塞的状态下中断,它的中断状态将被设置,然后根据被取消的操作来检查中断状态以判断是否中断(在取消点)。即如果不触发InterruptedException
那么中断状态将一直保持,直到明确清除中断状态。
因此如果任务代码能够响应中断,那么可以使用中断作为取消机制。
中断策略
中断策略规定线程如何解释某个中断请求——当发现中断请求时,应该做哪些工作,哪些工作单元对于中断来说是原子操作,以及以多快的速度来响应中断。
最合理的中断策略是某种形式的线程级取消操作或服务级取消操作:尽快退出,在必要时进行清理,通知某个所有者该线程已经退出。并可以建立其他中断策略,例如暂停服务或重新开始服务。
要区分线程与任务对中断的反应,可以是取消当前任务,也可以是关闭工作线程。
任务不会在其自己拥有的线程中执行,而是在某个服务(线程池)拥有的线程中执行。对于非线程所有者的代码(例如线程池,即任何在线程池实现以外的代码),应该小心保存中断状态,这样拥有线程的代码才能对中断做出响应。即大多数可阻塞的函数只是抛出InterruptException
的原因,将中断信息传递给调用方线程。
由于每个线程拥有自己的中断策略,因此除非你知道中断对该线程的含义,否则就不应该中断这个线程。
响应中断
有两种策略可以处理InterruptedException
- 传递异常,可能在执行某个特定于任务的清除操作后,从而使你的方法也成为可中断的阻塞方法
- throw
- 恢复中断状态,从而使调用栈中的上层代码能够对其进行处理
- 再次调用interrupt来恢复中断线程,即不能屏蔽
InterruptedException
- 再次调用interrupt来恢复中断线程,即不能屏蔽
只有实现了线程中断策略的代码才可以屏蔽中断请求,在常规的任务和库代码中都不应该屏蔽中断请求。
通过Future实现取消
处理不可中断的阻塞
许多可阻塞的方法都是通过提前返回或者抛出InterruptedException
来响应中断请求的,而并非所有的可阻塞方法或者阻塞机制都能响应中断。如果一个线程由于执行同步的socket IO或等待内置锁而阻塞,则中断没有任何作用。
线程阻塞的原因:
- 同步Socket IO。虽然read或write都不会响应中断,但是可以通过关闭套接字抛出
SocketException
- 同步IO。关闭一个正在
InterruptibleChannel
上等待的线程时,将抛出ClosedByInterruptException
,并关闭链路。 - Selector 异步IO。调用close或wake up会导致线程
CloasedSelectorException
- 获取某个锁。Lock类提供了lockInterruptibly方法,允许等待锁时可以中断。
采用newTaskFor封装非标准的取消
停止基于线程的服务
应用程序通常会创建拥有多个线程的服务,例如线程池,这些服务的生命周期通常比创建它们的方法的生命周期更长。如果应用程序结束,那么这些服务所拥有的线程也要结束。而且由于无法抢占式停止,因此需要它们自行结束。
正确的封装原则是:除非拥有某个线程,否则不能对该线程进行操控。线程有一个相应的所有者,即线程池,如果要中断这些线程,那么应该使用线程池。由于线程的所有权无法传递,因此服务应该提供生命周期方法来关闭他自己以及拥有的线程,因此当服务关闭时就会关闭所有的线程。