《java并发编程实战》读书笔记6--取消与关闭

这章的主要内容是关于如何使任务和线程安全,快速,可靠的停止下来。

7.1 任务取消

在Java中没有一种安全的抢占方式来停止线程,但是可以使用一些协作机制,比如:

让素数生成器运行1秒后取消(并不会刚好在运行1秒后停止,因为在请求取消的时刻和run方法中循环执行下一次检查之间可能存在延迟):

-7.1.1 中断

上面的取消方法有个重要的问题是:如果任务中调用了一个阻塞方法,例如BlockingQueue.put,那么任务可能永远不会检查取消标志,因此永远不会结束。比如:

前面第五章曾提到,一些特殊的阻塞库的方法支持中断。线程中断是一种协作机制,线程可以通过这种机制来通知另一个线程,告诉它在合适的或者可能的情况下停止当前工作,并转而执行其他的工作。

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

下面来解决之前BrokenPrimeProducer中永远检查不到标志位的问题:使用中断而不是boolean标志来请求取消

-7.1.2 中断策略

*对于非线程所有者的代码来说(例如,对于线程池而言,任何在线程池实现以外的代码),应该小心地保存中断状态,这样拥有线程的代码才能对中断作出响应。

*大多数可阻塞的库函数知识抛出interruptedException作为中断响应,尽快退出流程,并把中断信息传递给调用者,从而使调用栈中的上层代码可以采取进一步的操作。

*当检查到中断请求时,任务并不需要放弃所有的操作——它可以推迟处理中断请求,并直到某个更合适的时刻。因此需要记住中断请求,并在完成当前任务后抛出InterruptedException或者表示已收到中断请求。

-7.1.3 响应中断

对于一些不支持取消但仍可以调用中断的阻塞方法的操作,它们必须在循环中调用这些方法(interrput方法), 并在发现中断后重新尝试。在这种情况下应将中断状态保存在本地,并在返回前恢复状态而不是在捕获InterruptedException时恢复状态:

这部分有点看不明白。。。 先pass,以后再来钻研钻研

-7.1.4 示例:计时运行

给出了在指定时间内运行一个任意的Runnable的示例。在调用线程中运行任任务,并安排了一个取消任务,在运行指定的时间间隔后中断它。这解决了从任务中抛出为检查异常的问题,因为该异常会被timedRun的调用者捕获。下面的程序用到了ScheduledExecutorService。ScheduledExecutorService定时周期执行指定的任务

这是一种简单的方法,但却破坏了一下规则:在中断线程之前,应该了解它的中断策略。由于timedRun可以从任意一个线程调用,因此无法知道这个调用线程的中断策略(说到这里我好像把中断策略所针对的对象搞混淆了)。

在join方法返回后,它将检查任务中是否有异常抛出( task.rethrow() ) 如果有的话则会在timeRunde的线程中再次抛出异常。执行任务的线程拥有自己的执行策略,即使任务不响应中断,限时运行的方法(join)仍能返回到它的调用者。

join的不足:无法知道执行控制是因为线程正常退出而返回还是因为join超时而返回。

PS:这一小节看得有点云里雾里的,汉化质量实在是太垃圾了,哪天翻翻英文原版书,看看这块的解释。

-7.1.5 通过Future来实现取消

最后那句话是神马意思? 醉了,语文没学好的表示很蛋疼。

-7.1.6 处理不可中断的阻塞

在java的库中,许多可阻塞的方法都是通过提前返回或者抛出InterruptedException来响应中断请求的,从而使开发人员更容易构建出能响应取消请求的任务。然而,并非所有的可阻塞的方法或者阻塞机制都能响应中断,中断请求只能设置线程的中断状态,除此之外没有任何其他作用。对于那些由于执行不可中断操作而被阻塞的线程,可以使用类似于中断的手段来停止这些线程,但这要求我们必须知道线程阻塞的原因。

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

newTaskFor是一个工厂方法,它将创建Future来代表任务,还能返回一个RunnableFuture接口,该接口扩展了Future和Runnable(并有FutureTask实现)。通过定制表示任务的Future可以改变Future.cancel的行为。那个程序来演示:

这部分看得我有点云里雾里的, 以后刷二周目的时候再来仔细分析分析。

7.2 停止基于线程的服务

*应用程序通常会创建拥有多个线程的服务

*应用程序可以拥有服务,服务可以拥有工作线程,但应用程序并不能拥有工作线程,因此应用程序不能直接停止工作线程。

-7.2.1 示例:日志服务

现在还需要实现一种终止日志线程的方法,从而避免使JVM无法正常关闭。take方法能响应中断,如果将日志线程修改为当捕获到InterruptedException时退出,那么只需中断日志线程就能停止服务。然而,如果只是使日志线程退出,那么还不是一种完备的关闭机制。这种直接关闭的做法会丢失哪些正在等待被写入到日志的信息,而且其他线程在调用log时将被阻塞。另一种关闭LogWriter的方法是:设置某个“已请求关闭”标志,以避免进一步提交日志信息:

在收到关闭请求后,消费者会把队列中的所有消息写入日志,并解除所有在调用log时阻塞的生产者(我怎么感觉这里书这里和代码对不上号。。。)。然而这个方法中存在着竞态条件问题,使得该方法并不可靠。向LogWriter添加可靠的取消操作:

 -7.2.2 关闭ExecutorService

在复杂的程序中,通常会将ExecutorService封装在某个更高级别的服务中,并且该服务能提供其自己的生命周期方法。如:

-7.2.3 “毒丸”对象

另一种关闭生产者-消费者服务的方式是使用“毒丸”对象,当得到这个对象时立即停止。“毒丸”对象确保消费者在关闭之前首先完成了队列中的所有工作。来个例子:

只有在生产者和消费者的数量都已知的条件下,才可以使用“毒丸”对象。上述的解决方案可以扩展到多个生产者:只需每个生产者都向队列中放入一个毒丸对象,并且消费者仅当在接收到Nproducers个毒丸对象时才停止。

-7.2.4 示例:只执行一次分服务

下面程序的checkMail方法能在多台主机上并行地检查新邮件。它创建一个私有的Executor,并向每台主机提交一个任务。然后,当所有邮件检查任务都执行完成后,关闭Executor并等待结束。

ExecuteService提供的shutdown方法:平滑的关闭ExecutorService,当此方法被调用时,ExecutorService停止接收新的任务并且等待已经提交的任务(包含提交正在执行和提交未执行)执行完成。当所有提交任务执行完毕,线程池即被关闭。

awaitTermination方法:接收timeout和TimeUnit两个参数,用于设定超时时间及单位。当等待超过设定时间时,会监测ExecutorService是否已经关闭,若关闭则返回true,否则返回false。一般情况下会和shutdown方法组合使用。

-7.2.5 shutdownNow的局限性

当通过shutdownNow来强行关闭ExecuteService时,它会尝试取消正在执行的任务,并返回所有已提交但尚未开始的任务,从而将这些任务写入日志或者保存起来以便之后进行处理。但是,我们无法通过常规方法来找出哪些任务已经开始但尚未结束。意思就是虽然shutdownNow会返回所有已提交但尚未开始的任务(都是Runnable), 但是却返回不了已经在执行但还未结束的任务,只能将这些任务取消掉。有的时候我们需要知道并保存这个状态,所以不仅要知道哪些任务还没有开始,而且还要知道哪些正在执行的任务还没有完成。

下面的程序给出了如何在关闭过程中判断正在执行的任务。

在程序清单7-22的WebCrawler中给出了TrackingExecutor的用法。网页爬虫程序的工作通常是无穷无尽的,因此当爬虫程序关闭时,我们通常希望保存它的状态(这里的意思就是要保存程序正在处理的网页),以便稍后重新启功时继续处理这些网页。

在TrackingExecutor中存在一个不可避免的竞态条件,从而产生误报问题:一些认为已取消的任务实际上已经执行完成。原因是在任务执行最后一条指令以及线程池将任务记录为“结束”的两个时刻之间,线程池可能被关闭。如果任务是幂等的(即将任务执行两次的结果与执行一次会得到相同的结果),那么这不会存在问题,否则需考虑这种风险。

7.3 处理非正常的线程中止

首先应该明确的是导致线程提前死亡的最主要原因是RuntimeException,如何处理这种非正常的线程中止来确保多线程情况下的线程安全性呢?简而言之就是利用捕获异常处理器和故障通知机制。下面的程序给出了如何在线程池内部构建一个工作者线程:

上面的是一种主动的方法来解决为检查异常。在Thread API中提供了UncaughtExceptionHandler,它能检测出由于未捕获的异常而终结的情况。当一个线程由于未捕获异常而退出时,JVM会把这个事件报告给UncaughtExceptionHandler异常处理器。

最常见的响应方式是将一个错误信息以及相应的栈追踪信息写入应用程序中:

要为线程池中的所有线程设置一个UncaughtExceptionHandler,需要为ThreadPoolExecutor的构造函数提供一个ThreadFactory。

7.4 JVM关闭

-7.4.1 关闭钩子

正常的JVM关闭中,JVM首先调用所有已注册的关闭钩子。这个关闭钩子指的是通过Runtime.addShutdownHook注册但尚未开始的线程。后面看不明白了,pass....

-7.4.2 守护线程

线程可以分为两种:普通线程和守护线程。在JVM启动时创建的所有线程中,除了主线程以外,其他的线程都是守护线程(如垃圾回收器以及其他执行辅助工作的线程)。当创建一个新线程时,新线程将继承创建它的线程的守护状态,因此在默认情况下,主线程创建的所有线程都是普通线程。当一个线程退出时,JVM会检查其他正在运行的线程,如果这些线程都是守护线程,那么JVM会正常退出。当JVM停止时,所有仍然存在的守护线程都将被抛弃,既不会执行finally代码块,也不会执行回卷栈。

应该尽可能少使用守护线程——很少有操作能够在不进行清理的情况下被安全地抛弃。特别是,如果在守护线程中执行可能包含I/O操作的任务将会是一种危险的行为。

-7.4.3 终结器

有些资源如文件句柄或套接字,当不需要时必须现实地交还给操作系统,而不是通过垃圾回收器回收。为了实现这个功能,垃圾回收器对哪些定义了finalize方法的对象会经行特殊处理:在回收器释放它们后。调用它们的finalize方法,从而保证一些持久化的资源被释放。大多数情况下,通过使用finally代码块和显示的close方法,能够比终结器更好的管理资源,所以要避免使用终结器。



时间: 2024-10-11 17:30:21

《java并发编程实战》读书笔记6--取消与关闭的相关文章

java并发编程实战学习笔记之取消与关闭

第七章 取消与关闭 7.1 任务取消 方式一.通过volatile类型的域来保存取消状态 方式二.interrupt()方法 interrupt()可以中断目标线程 isinterrupted()方法用来检测目标线程的中断状态 interrupted()用于清除中断状态,并且返回之前的中断状态,这是唯一可以清除中断状态的方法,如果在调用该方法是返回了true,那么除非你想屏蔽这个中断,否则你必须对他进行处理,可以抛出interruptExeption异常或者重新通过interrupt来恢复中断状

JAVA并发编程实战 读书笔记(二)对象的共享

<java并发编程实战>读书摘要 birdhack 2015年1月2日 对象的共享 JAVA并发编程实战读书笔记 我们已经知道了同步代码块和同步方法可以确保以原子的方式执行操作,但一种常见的误解是,认为关键之synchronized只能用于实现原子性或者确定临界区.同步还有另一个重要的方面:内存可见性. 1.可见性 为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制. 在没有同步的情况下,编译器.处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整.在缺乏足够同步的多线程程

JAVA并发编程实战 读书笔记(一)线程安全性

线程安全性   1.什么是线程安全 在线程安全的定义中,最核心的概念是正确性.正确性的含义是,某个类的行为与规范完全一致.当对正确性有了一个比较清晰的定义后,就可以定义线程安全性:当多个线程访问某个类时,这个类始终能表现出正确的行为,那这个类就是线程安全的. 举例:无状态对象一定是线程安全的. 大多数Servlet都是无状态的,当Servlet在处理请求时需要保存一些信息时,线程安全才会成为一个问题. 2.原子性 举个例子:语句 ++i:虽然递增操作++i是一种紧凑的语法,使其看上去是一个操作,

《java并发编程实战》笔记(一)

最近在看<java并发编程实战>,希望自己有毅力把它读完. 线程本身有很多优势,比如可以发挥多处理器的强大能力.建模更加简单.简化异步事件的处理.使用户界面的相应更加灵敏,但是更多的需要程序猿面对的是安全性问题.看下面例子: public class UnsafeSequence { private int value; /*返回一个唯一的数值*/ public int getNext(){ return value++; } } UnsafeSequence的问题在于,如果执行时机不对,那么

Java并发编程实践读书笔记--第一部分 基础知识

目前关于线程安全性没有一个统一的定义,作者自己总结了一个定义,如下:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么就称这个类是线程安全的. 在并发编程中,由于不恰当的执行时序而出现不确定的结果的情况被称为竞态条件(Race Condition).最常见的竞态条件就是“先检查后执行(Check-Then-Act)”操作,即通过一个可能已经失效的观察来决定下一步的动作.比较简单的例子就是两

《Java并发编程实战》笔记-取消与关闭

1,中断是实现取消的最合理方式.2,对中断操作的正确理解是:它并不会真正地中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己.3,区分任务和线程对中断的反应是很重要的4,线程有一个相应的所有者,即创建该线程的类.5,除非拥有某个线程,否则不能对该线程进行操控.由于每个线程拥有各自的中断策略,因此除非你知道中断对该线程的含义,否则就不应该中断这个线程.

java并发编程实战学习笔记之任务执行

第六章 任务执行 6.1 在线程中执行任务 串行->多线程->有限个多线程 6.2 executor框架 通过有界队列可以防止高负荷程序过度消耗内存 java.lang.concurrent提供了线程池作为实现executor框架的一部分 executor接口:提供生产者-消费者模式 基于executor的web服务器: public class TaskExecutionWebServer {     private static final int NTHREADS = 100;     

java并发编程实战学习笔记之对象的组合与基础构建模块

第四章 对象的组合 4.1 构建安全的类 4.2 实例封闭 @ThreadSafe public class PersonSet {     @GuardedBy("this") private final Set<Person> mySet = new HashSet<Person>();     public synchronized void addPerson(Person p) {         mySet.add(p);     }     pub

Java并发编程实践读书笔记(5) 线程池的使用

Executor与Task的耦合性 1,除非线程池很非常大,否则一个Task不要依赖同一个线程服务中的另外一个Task,因为这样容易造成死锁: 2,线程的执行是并行的,所以在设计Task的时候要考虑到线程安全问题.如果你认为只会在单任务线程的Executor中运行的话,从设计上讲这就已经耦合了. 3,长时间的任务有可能会影响到其他任务的执行效率,可以让其他线程在等待的时候限定一下等待时间.不要无限制地等待下去. 确定线程池的大小 给出如下定义: 要使CPU达到期望的使用率,线程池的大小应设置为:

java并发编程实战学习笔记之第三部分:活跃性、性能与测试

第十章 避免活跃性危险 锁顺序死锁:定义锁的顺序,可以通过某种方法决定每个锁的顺序,比如hashcode或者序列号之类的 在锁的调用顺序不是很明显的情况下,在持有锁的情况下调用其他外部方法一定要注意,可以通过开放调用,避免发生死锁的危险,即使用同步代码块保护仅仅保护那些共享变量即可,但这种降低锁粒度的方法可能会使得原本大的代码块失去原子性,解决办法为:将服务的状态改为关闭之前一直持有锁,状态改变之后,其他线程也就能够看到关闭信息从而不会再次执行关闭操作... 死锁的诊断与避免:    通过try