《java并发编程实战》读笔 结构化篇

第六章 任务执行

6.2 Executor框架

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

         在Java类库中,任务执行的主要抽象不是Thread,而是Executor。如图:


         它提供了一种标准的方法将任务的提交过程与执行过程解耦。Executor的实现还提供了对生命周期的支持,以及统计信息收集、应用程序管理机制和性能监视等机制。


6.2.3线程池

         管理一组同构工作线程的资源池。可以通过调用Executors中的静态工厂方法来创建一个线程池。

6.2.4 Executor的生命周期

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

         为了解决执行服务的生命周期问题,Executor扩展了ExecutorService接口,添加了一些用于生命周期管理的方法。

         4个生命周期阶段:创建、提交、开始、完成。

6.2.5延迟任务与周期任务

         Timmer类负责管理延迟任务以及周期任务。

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

         Timer问题2:如果TimerTask抛出了一个未检查异常,那么Timer线程不捕获异常。因此TimerTask抛出未检查异常时将终止定时线程。以后的任务将不再执行——Thread Leakage线程泄漏。ScheduledThreadPoolExecutor能正确处理这些表现出错误行为的任务。在Java5.0或更高的JDK中,将很少使用Timer..

         如果要构建自己的调试服务,可以使用DelayQueue,实现了BlockingQueue。并为ScheduledThreadPoolExecutor提供调试功能。

6.3.2携带结果的任务Callable与Future

         许多任务实际上都是存在延迟计算——执行数据库查询,从网络上获取资源……

         Runnable和Callable描述的都是抽象的计算任务。Future表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等。Get方法会立即返回或者抛出异常,如果没完成则阻塞直到任务完成;如果抛出异常则被封装为ExecutionException并重新抛出(通过getCause来获得被封装的初始异常);如果任务被取消抛出CancellationException。

        

·java6开始,ExecutorService实现可以改写AbstractExecutorService中的newtaskFor方法。

6.3.3示例:使用Future实现页面渲染器

 

待提升点:用户不必等到所有图像都下载完成,而希望下载完一幅就立即显示出来。

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

         如果没有在相似任务之间找出细粒度的并行性,那么这种方法带来的好处将减少。

         只有当大量相互独立且同构的任务可以并发进行处理时,才能体现出将程序的工作负载分配到多个任务中带来的真正性能提升。

6.3.5 CompletionService:Executor与BlockingQueue

         CompletionService将Executor和BlockingQueue的功能融合在一起

         ExecutorCompletionService的实现非常简单。构造函数中创建BlockingQueue来保存计算完成的结果。完成时调用FutureTask中的done。当提交某任务时,首先包装为一个QueueingFuture,这是FutureTask的子类,然后改写子类的done方法,将结果放入BlockingQueue。

6.3.6示例:使用CompletionService实现页面渲染器

多个ExecutorCompletionService可以共享一个Executor。因此可以创建一个对于特定计算的私有。又能共享一个公共Executor的ExecutorCompletionService.因此,CompletionService的作用相当于一组计算的句柄,这与Future作为单个计算的句柄是非常类似的。

6.3.6 为任务设置时限

         注意:当任务超时后应该立即停止,避免浪费计算资源。可两次使用Future,如果get方法抛出TimeoutExceptioin,可以通过Future来取消任务。

第七章 取消与关闭

         Java没有提供任何机制来数例地终止线程。

7.1任务取消

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

         一:设置某个CancellationRequested标志,任务定期查看标志,如果true提前结束。

         一个可取消的任务必须拥有取消策略,这个策略中将详细地定义取消操作的“How””when””what”

         应用例子:Stop-Payment。停止支付。

7.1.1中断

         因为可能无法检查这cancelled标志,所以……

         每个线程都有一个boolean类型的中断状态。

         JVM并不能保证阻塞方法检测到中断的速度,但实际情况中响应速度还是非常快的。

         阻塞库方法,如:Thread.sleep,和Object.wait,会检查线程何时中断,并在发现中断时提前返回。它们在响应中断时执行:清除中断状态,抛出InterruptedException。

         中断的正确理解:它并不会真正地中断一个正在运行的线程,只是发出中断请求,然后由线程在下一个合适的时刻中断自已。

         使用interrupted时小心,它会清除线程的中断状态,如果调用返回了true,除非想屏蔽这个中断,否则必须对其处理——抛出或恢复。

         通常:中断是实现取消的最合理方式。

7.1.2 中断策略

         合理的中断策略是线程级或服务级取消操作。

7.1.4 示例:计时运行

 

         破坏了规则:在中断线程之前应该了解它的中断策略。由于timeRun可以从任意线程中调用,因此它无法知道该线程的中断策略。

         如果任务超时之前完成,并返回。我们不知道将会运行什么代码

 

         由于依赖限时的join,无法知道执行控制是因为线程正常退出还是因为Join超时返回。

7.1.5通过Future来实现取消

         ExecutorService.submit将返回一个Future来描述任务。Future拥有一个cancel方法。带boolean类型的参数mayInterruptIfRunning表示取消操作是否成功。

7.1.6处理不可中断的阻塞

         并非所有的可阻塞方法或者阻塞机制都能响应中断:如果一个线程由于执行同步的Socket I/O或者等待获得内置锁而阻塞,那么中断请求只能设置线程的中断状态无任何作用。

         Java.io包中的同步SocketI/O,虽然InputStream和OutputStream中的read与write都不会响应中断,但通过关闭底层的套接字可以使得执行read或write等方法而被阻塞的线程抛出一个SocketException。

         同步I/O:当中断一个正在InterruptibleChannel上等待的线程时将抛出ClosedByInterruptException并关闭链路。

         Selector异步I/O。如果线程调用Selector.select时阻塞了,那调用close或wakeup会抛出ClosedSelectorException并提前返回。

·获取某个锁,如果线程等待某个内置锁而阻塞,将无法响应中断。但Lock提供了lockInterruptibly方法,允许在等待锁的同时仍能响应中断。

7.1.7采用newTaskFor来封装非标准的取消

         通过定制表示任务的Future可以改变Future.cancel的行为。例如:定制取消代码实现日志记录或者收集取消操作的统计信息.

7.2停止基于线程的服务

         例如:线程池,由于无法通过抢占式的方法来停止线程,因此它需要自行结束。

         正确的封装原则:除非拥有某个线程,否则不能对该线程进行操控。

         线程所有权正式定义:线程由Thread对象表示,并且像其他对象一样可以被自由共享。线程有一个相应的所有者——创建该线程的类。

         与其他封装对象一样,线程的所有权是不可传递的。应用程序拥有服务,服务拥有工作者线程。但应用程序并不能拥有工作者线程。故,应用训育不能直接停止工作者线程。

         对于持有线程的服务,只要服务的存在时间大于创建线程的方法的存在时间,那么就应该提供生命周期方法。(用来shutdown!)

7.2.1示例:日志

         为避免使JVM无法正常关闭:将日志线程修改为当捕获interruptedException时退出,那么只要中断就停止服用。(BolockingQueue.take取出生产者队列能响应中断)

         不足点直接关闭会丢失日志。

         方法二:设置“已请求关闭”标志,以避免进一步提交日志消息。

将管理线程的工作委托给一个ExecutorService,通过封装ExecutorService,将所有权链(Ownership Chain)从应用程序扩展到服务以及线程,所有权连上的各个成员都将管理它所拥有的服务或线程的生命周期。

7.2.3“毒丸”对象(Poison Pill)

         一个放在队列上的对象——“当得到这个对象时,立即停止”

         注意:在无界队列中使用。

7.2.5 shutdownNow的局限性

         通过shutdownNow强行关闭ExecutorService时,它尝试取消正在执行的任务,并返回所有已提交但尚未开始的任务。然而,我们无法通过常规方法来找出哪些任务已经开始但尚未结束。

7.3处理非正常情况线程中止

         导致线程提前死亡主因RuntimeException。


未捕获异常的处理。

         ThreadAPI提供了UncaughtExceptionHandler,它能检测出某个线程由于未捕获的异常而终结的情况。通过两种方法互补能有效防止线程泄漏问题。

         当线程由于未捕获异常而退出,JVM会把这个事件报告给异常处理器。如果没有异常处理器,那默认输出到System.err。

7.4 JVM关闭

         正常关闭的触发方式:

1)  当最后一个非守护线程结束时;

2)  调用了System.exit时

3)  通过特定平台方法关闭时(键入Ctrl-C)

还可以“结束任务”,在操作系统中杀死进程

7.4.1关闭钩子(shutdownHook)

         指通过Runtime.addShutdownHook注册但尚未开始的线程。JVM并不能保证关闭钩子的调用顺序。在关闭应用程序时,如果线程仍然运行,它将与关闭进程并发执行。……JVM最终结束时,这些线程被强行结束。……(可能没成功结束),当被强行关闭时,只是关闭JVM而不运行关闭钩子。

         关闭钩子应该是线程安全的;不应该对应用程序的状态或者JVM的关闭原因做出任何假设;必须尽快退出。可以用于实现服务或应用程序的清理工作。

 

7.4.2守护线程(DaemonThread)

         Use:希望创建一个线程来执行一些辅助工作,但又不希望其阻碍JVM的关闭。

         线程分两种:普通线程和守护线程。主线程创建的所有线程都是变通线程。

         当JVM停止时,所有仍然存在的守护线程都将被抛弃——既不会执行finally代码块也不会执行回卷栈。

         尽可能少地使用守护线程。特别是,如果守护线程中执行可能包含I/0操作的任务,将是一种危险行为。最好用于执行“内部”任务,如周期性从内存的缓存中移除逾期的数据。

7.4.3 终结器

         当不再需要内存资源时,可以通过垃圾回收器来回收它们,但对于其他一些资源,如文件句柄或套接字句柄,当不需要它们时,必须显式地交还给操作系统。于是,垃圾回收器对定义了finalize的方法的对象进行特殊处理:回收器释放它们后,调用它们的finalize方法,保证一些持久化的资源被释放。

         由于终结器可以在某个由JVM管理的线程中运行,所以必须对其访问操作进行同步,复杂的终结器在对象上产生巨大性能开锁。大多数通过finally块和显式close方法。(唯一例外:需要管理对象持有的资源通过本地方法获得的)

         避免编写使用包含终结器的类。

小结

         使用FutureTask和Executor框架,可以帮助我们构建可取消的任务和服务。

猜你喜欢

转载自blog.csdn.net/lylhjh/article/details/53635306