第七章 取消和关闭

java没有提供任何机制来安全地终止线程,但他提供了中断,这是一种协作机制
因为直接中断线程是一个非常危险的操作,所以通过中断机制;让任务自己来响应中断、结束工作是一种更加安全的方式。

生命周期结束的问题非常复杂,但却很重要

一 任务取消

java没有一种安全的抢占式方法来停止线程,只有一种协作式的机制。使请求取消任务和代码都遵循一种协商好的协议。

一种协作机制之一
d设置一个已取消的标志,定时任务定期检查该标志,如果true则提前结束.

1.1 中断

问题:
任务中如果执行了一个阻塞操作,则检查标志的代码可能不会执行到

一些特殊的阻塞库的方法支持中断(应该是方法内部在阻塞过程中,也会检查线程的状态).比如BlockingQueue的take方法、Object的wait等等

每个线程都有一个Boolean类型的中断状态
jvm不能保证检测到中断的速度,一般还是非常快的

静态的interrupted方法是唯一清除当前线程的中断状态的方法。

像一些能响应中断的阻塞方法,如:Thread的sleep等方法的操作流程

  1. 清除中断状态
  2. 抛出InterruptedException,表示阻塞操作由于中断而提前结束

线程非阻塞状态下中断时,它的中断状态将被设置,然后根据将被取消的操作来检查中断状态判断发生了中断.通过这样的方法,中断操作将变得有粘性–如果部触发InterruptedException,那么中断状态将一致保持,直到明确地清除中断状态

核心

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

1.2 线程中断策略

前面写的都是任务中处理中断的方式,现在说一下线程如何处理中断的策略

区分任务和线程对中断的反应是很重要的。因为任务一般不会在某个自己拥有的线程中执行,所以我们不应该去操心设置改变线程对中断的处理策略(是直接将线程死掉还是怎么怎么..).所以最好的方式:[抛出异常(如阻塞库中的部分方法)]并将错误信息传递给调用者,从而使上层的代码来进行处理
小结:如果你捕获了某个InterruptedException,并且其他的代码需要判断线程的中断状态,则我们不要忘了传递中断状态

1.3 响应中断

两种策略

  1. 传递异常,使自己的方法也成为可中断的阻塞方法
  2. 恢复中断状态,从而使调用栈中的上层代码能对其进行处理

1.4 计时运行

如果我们想要定时取消一个任务:我们可能会拿到这个线程的引用,然后定时调用interrupt方法。但这有个隐患:因为我们不知道这个调用线程的中断策略:可能会出现之前想要中断的任务已经运行结束,这个线程现在在运行其他任务。这个时候就太可怕了!!!所以尽量不要这么做

1.5 通过Future来实现取消

通过Future的cancel方法

1.6 处理不可中断的阻塞

在java中还是有许多阻塞方法是不响应中断的,比如Socket中的io阻塞。
我们可以通过一些小手段来实现中断,比如IO阻塞时,我们可以调用socket.close()直接关闭阻塞.

具体的做法:重写Thread的interrupt方法

publuc void interrupt(){
	try{
		socket.close();
	}catch(IOException e){
		
	}finally(){
		super.interrupt();//重要
	}
}

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

大部分情况下,我们不会自己创建线程来执行任务,而是把任务交给一个线程池来执行。为了解决这种情况,java6在ThreadPoolExecutor中新增了一个功能:当把一个Callable提交给ExecutorService时,submit方法会返回一个Future,我们可以通过这个FUture来取消任务。newTaskFor是一个工厂方法,它将创建Future来代表任务.该方法还能返回一个RunnableFuture接口,该接口扩展了Runnable和Future(并由FutureTask实现)

我们可以定制复合我们需求的Future,重写其中的方法(比如日志记录等等)

如何实现呢?

  1. 重写newTaskFor方法,实现对特殊Callable对象返回特殊的Future
  2. 特俗的RunnableFuture重写cancel方法,并且最后调用父类的cancel方法
public class FutureTask<V> implements RunnableFuture<V> {


小结:
1.6中式通过Thread的方法来中断,这节是用FutureTask来实现

二 停止基于线程的服务

应用程序通常会创建拥有多个线程的服务,比如:线程池,并且这些服务的生命周期通常比创建他们的方法生命周期更长。
我们需要考虑当应用程序准备退出时,如何停止这些多线程的服务?

核心思想:对于持有线程的服务,只要服务的存在时间大于创建线程的方法的存在时间,那么就应该提供生命周期方法

2.1 示例:日志服务

我们记日志,一般喜欢调用System.out.println().(不考虑日志框架)。这种方法在一些对性能要求比较高的应用中,大量的这些代码会带来性能问题。

我们可以封装一个日志类,这个类带有一个BlockingQueue,通过这个类来实现高性能的日志框架(基于多生产者低消费者模式)
在这里插入图片描述

关闭问题

直接关闭LoggerThread很简单,因为take方法支持响应中断。但还有很多问题,比如

  1. 队列中可能还有部分消息没被打印
  2. 可能有部分线程在put时阻塞了,你这把消费者一关,其他所有阻塞着的生产者可能会一直卡住

优化方法
增加一个字段-记录产品的数量,只有当产品都消费完了并且当前生产者被告知需要结束了才结束消费者线程
在这里插入图片描述
在这里插入图片描述

2.2 关闭ExecutorService

提供了两种关闭方法

  • shutdown
  • shutdownNow
    本节的主要思想
    我们可以依赖ExecutorService的实现类,因为ExecutorService本身已经实现了关闭方法

2.3 毒丸现象

很简单,就是当消费者得到一个毒丸对象时,就可以结束了

三 处理非正常的线程终止

在多线程程序中,线程发生故障,可能不会在控制台中报错,而且也不会有人一直观察控制台。
我们需要一种合适的手段来监测线程的运行情况,防止线程死了都不知道

线程发生故障,最多情况就是代码里面抛出了一个RuntimeException.
所以解决办法一:在调用代码的外面加一个try-catch块,捕获到RuntimeException然后再自定义处理(甚至ThreadPoolExecutor和swing就是这么做的)

方法二:通过UncaughtExceptionHandler
如果想要为线程池中的所有线程设置一个UncaughtExceptionHandler,则需要为ThreadPoolExecutor的构造函数提供一个ThreadFactory

小心
在线程池中,UncaughtExceptionHandler只对execute方法抛出的异常有效;submit提交的任务,无论抛出什么异常都会被认定为任务返回状态的一部分。只能通过get获取异常

四 JVM关闭

分为正常关闭和强行关闭两种

4.1 关闭钩子

  • 我们可以通过增加钩子来实现:在JVM关闭时,做一些操作。
  • 钩子对强行关闭无效

4.2 守护线程

  • 线程分两种:普通线程和守护线程
  • 在JVM启动时,除了主线程,其他都是守护线程(GC等等)
  • 新线程继承创建它的线程的守护状态
  • 两种线程的唯一区别:当线程退出时发生的操作

4.3 终结器(finalize())

  • 避免使用终结器
  • 使用同步
  • 资源管理最后用finally来实现
发布了82 篇原创文章 · 获赞 1 · 访问量 1993

猜你喜欢

转载自blog.csdn.net/m0_38060977/article/details/102990593
今日推荐