Java并发编程实战笔记(6)-取消与关闭

Posted by zhidaliao on February 5, 2016

简介

Java没有提供任务安全结束线程的机制,提供了中断,这是一种协作机制:使一个线程终止另一个线程。

为什么是协作机制:1、立即停止会造成数据结构的不一致性 2、任务本身比其他线程更懂得如何清除当前正在执行的任务

软件质量的区别:良好的软件能很好的处理失败、关闭、结束等过程。

任务取消

外部代码,能够将某个操作正常完成之前,将其置入完成状态,那么这个操作就称为 可取消的。

协作机制 - 取消标志

设置某个 取消标志 , 任务每次执行前都查询一次,如果标志生效,任务将提前结束。

while(!isStop){
	...
}

一个可取消的任务必须拥有取消策略,定义了

  • How: 其他线程怎么取消任务
  • When: 什么时候检查是否被要求取消
  • What: 检查到取消标志后,该执行什么操作

场景: 银行支付:如何停止正在支付的交易,检查到停止指令后要遵守什么流程,事务回滚,通知。

协作机制 - 中断

错误的协作机制:

while(!isStop){
	...
	queue.take();  //阻塞方法,永远也无法检测到标志 isStop
	...
}

线程中断是一种协作机制,线程可以通过这种机制来通知另一个线程,告诉它在合适的情况下停止当前工作。

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

  • interrupt 能中断线程,
  • isInterrupt 查询中断的状态。
  • interrupted 返回中断状态,然后清除当前线程的中断状态

一些阻塞库方法支持中断,比如 thread.sleep()await(),会检查中断状态,发现了之后提前返回,响应中断的操作包括:

  • 清除中断状态
  • 抛出 InterruptException 表示 阻塞操作是中断而停止的

核心知识点:

  • JVM不能保证阻塞方法检测到中断的速度,但是一般响应都挺快的。
  • 线程在非阻塞状态下中断时,它的中断状态将被设置; 如果不触发InterruptedException , 那么中断状态将一直保持,直到明确地清除中断状态。
  • 调用 interrupt 并不意味着马上停止目标线程正在进行的工作,只是传递了请求中断的信息。
  • 有些方法,比如sleep/wait/join, 在执行前或者执行当中检测到中断请求,会抛出 interruptedException,
  • 设计良好的方法可以完全忽略中断请求,只要调用代码能对中断请求进行某种处理 ; 设计糟糕的方法会屏蔽中断请求
  • 使用 interrupted 方法时,要对异常进行处理,要么抛出 interruptedException ,要么 interrupt 重新恢复中断状态

改进:

// 判断线程中断 在queue方法之前检测   为了提高中断的响应
while(!Thread.currentThread().isInterrupted() ){
	...
	queue.take();  //阻塞方法,能够检测到线程中断
	...
}
中断策略
  • 合理的线程中断策略: 尽快退出 ; 在必要时进行清理 ; 通知所有者该线程已经停止。
  • 大多数可阻塞的库函数都只是抛出 InterruptedException 作为中断响应:尽快退出,并把中断信息传递给调用者。
  • 当检查到中断请求, 不需要立刻结束当前任务,可以推迟处理中断请求,在完成任务后抛出 InterruptedException 或者表示已收到请求,这种方式能够避免中断的时候数据结构被破坏。
  • 线程应该只能由其所有者中断,然后所有者会将中断的处理封装到某个合适的取消机制。
响应中断

当调用可中断函数,有两种策略可以处理 InterruptedException:

  • 传递异常(在清除操作之后),使你的方法也变成可阻塞函数
  • 恢复中断状态。(让上层处理)

重点是,不能屏蔽异常,除非线程实现了中断策略。

场景1:不支持取消,但仍可以调用可阻塞方法的操作。

  • 必须在循环中调用可阻塞的方法
  • 在本地保存中断状态
  • 在方法返回前恢复中断状态,而不是在捕获到 InterruptException 时恢复状态 (不然会引起无线循环,大多数中断方法会在入口处检查中断状态,该状态被设置时会抛出 InterruptedException
public Task test{

	private boolean intered = false;
	
	try{
		while(true){
			try{
				return queue.take();	// 重点:检测到中断,转入 finally 代码块
			}catch( InterruptException e){
				intered = true;
			}
		}
	}finally{
		if( intered ){
			Thread.currentThread.interrupt();	
		}	
	}
}

如果代码不调用 可阻塞的库函数,也可以通过轮询 线程的中断状态 来响应中断,就是响应性会更弱。需要考虑

场景2:使用Future超时取消线程

cancel(boolean mayInterruptIfRunning):

  • true: 如果任务运行中,将会被中断
  • true: 如果任务已经结束,也不会有影响
  • false: 若任务没有启动,就不要运行它。

哪些场景可以中断:

  • 了解线程的中断策略,否则不要轻易取消;
  • 使用标准的 Excutor创建的,并通过他们的 Future 使用cancel(true) 取消任务。
处理不可中断的阻塞:

可阻塞的库函数,会提前退出,或者清楚中断然后抛出 InterruptedException ; 执行同步的 Socket IO或者等待获得锁的线程,除了能设置线程的 中断状态,不会做其他操作。

对于阻塞 不可中断的函数,我们要采取类似中断的手段来停止线程

Socket I/O编程,InputStream read 不会响应中断,但是通过关闭底层的socket,能迫使read抛出异常,达到中断的效果

停止 基于线程的服务

应用程序通常会有创建多个线程的服务,这个服务就是线程池,应用程序关闭的时候,需要线程池中运行的线程自行关闭。

封装原则:

  • 如果不是线程的拥有者,不能对该线程进行操控, 比如应用程序的线程池,就是线程的拥有者。必须提供生命周期的方法管理线程。
  • 拥有权不能被传递,应用程序拥有线程池 , 线程池拥有线程 , 但是不允许 应用程序关闭线程。
场景: 构建日志服务

生产者打印日志放入 queue中 , 消费者在线程中取queue 保存日志。

  • 方案A:日志写入的线程检测 InterruptedException,然后停止服务 ; 缺点:不能保证生产者停止服务 / 队列中没有消费的信息会丢失
  • 方案B:生产者设置一个标志,在写入日志之前检查。但是还是会出现时间差,写入队列的下一时刻 消费者结束了
  • 方案C:在生产者的 stop方法中的 原子性的设置标志; 创建一个计数器,生产入queue方法每执行一次+1 ; 消费者的停止方法要检测 计算器是否为0 ;

------------------------- A -------------------------

void stop(){
	synchronized(this){
		isShutdown = true;
	}
}

void create(){
	if(isShutdown){
		return;
	}
	queue.put();
	count ++;
}
------------------------- B -------------------------
void stopComsumer(){
	while(true){
		synchronized(A.this){
			if(isShutdown && count == 0){
				// break;
			}
		}
	}
}
毒丸对象

关闭生产者-消费者的一种方式:产生特定元素,读取到之后停止工作。 适用场景:

  • 生产者和消费者数量已知的情况
  • 无界队列才比较可靠
  • 消费者和生产者的数量不能过多
shutDownNow 的注意事项:

尝试取消所有开始的任务,不再启动尚未开始(可能是已接受的新任务)执行的任务。调用 shutDownNow 时会返回所有未开始启动的任务

NOTE:需要保存, Executor关闭时,正在运行状态的任务。

void execute(Runnable runble){
	// 创建一个新线程运行 任务;
	exec.execute( new Runnable(){
		public void run(){
			try{
				runble.run();
			}finally{
				// 重点,代表线程池已经关闭,  但是子线程是中断退出
				if(!shutDown && thread.currentThread.isInterrupted()){	
					// collection();
				}
			}
		}
	});
}
处理 非正常的线程终止

场景:当子线程因为一个未捕获的异常停止时,会输出相关的异常,但是不会对主程序造成影响,导致异常线程被忽略.

线程提前死亡的原因一般是因为 RuntimeException ,这个异常一般是因为 编程错误 或者 其他不可修复的异常, 一般不会被捕获,不会在调用栈中被逐层传递

方案A:在线程池内部运行任务 或 第三方不可靠服务的 伪代码:

try{
	while(!isStop){
		runTask();
	}
}catch(Throwable e){
	// 
}finally{
	// 通知 后续操作
}

方案B:通过 被动的 UncaughtExceptionHandler 处理异常(推荐和方案A结合使用)

  • 当线程由于未捕获异常关闭的时候, JVM会把异常报告给 应用程序的 UncaughtExceptionHandler 处理。
  • 要为线程池的所有线程设置 UncaughtExceptionHandler,需要为 ThreadPoolExecutor的构造函数提供一个 ThreadFactory.
  • 只有通过 execute提交的方法,才会被异常处理器捕获,submit提交的任务, 无论是什么异常,都会被当作返回状态的一部分,异常将会被 get 方法以 ExecutionException 的形式抛出
public static void main(String[] args) {

	Thread th = new Thread(new Runnable() {
		public void run() {
			BigDecimal bd = new BigDecimal("1");
			bd.divide(BigDecimal.ZERO);
		}
	});

	th.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
		@Override
		public void uncaughtException(Thread t, Throwable e) {
			System.out.println("exception ~~ " + t.getState() + "~~" + e.getMessage() );
		}
	});

	th.start();
}

JVM 关闭

正常关闭的标准方式:

  • 最后一个 非守护 线程结束
  • 调用 System.exit
  • 特定平台的方式: 发送 SIGNT信号 或 CRTL+C

强行关闭:调用Runtime / halt / SIGKILL

关闭钩子

定义: 通过Runtime.addShutdownHook注册但尚未开始的线程

  • JVM正常关闭中,会调用所有已注册的关闭钩子,不保证调用顺序。
  • 等所有注册的钩子结束后,如果 runFinalizersOnExit为true , JVM将运行 终结器, 然后再停止。
  • JVM停止后,所有的还在运行的非守护线程会被强制结束(钩子肯定不会被执行)。
  • 如果 关闭钩子或终结器 没有执行完毕,那么正常关闭进行“挂起”,并且JVM必须被关闭
  • shutdown hook必须是线程安全的。必须尽快退出,不要延迟JVM的关闭时间,可以用来实现服务或应用程序的清理工作。
  • 如果 hook依赖的对象被其他hook 依赖, 最好使用同一个 hook,串行的业务处理会避免很多的故障。
Runtime.getRuntime().addShutdownHook(new Thread(){
	@Override
	public void run(){
		System.out.println("线程的后续处理 。 。 。 ");
	}
});
线程分类: 普通线程 & 守护线程
  • 守护线程的场景:创建一个线程处理辅助工作,但是不希望阻碍JVM的关闭,使用方法:setDaemon(true);
  • JVM启动运行的时候,除了主线程之外,其他都是守护线程(垃圾回收线程等。。。),主线程创建的线程会继承普通线程的属性
  • 尽可能不要创建守护线程
  • 这种线程的优先级非常低,通常在程序里没有其他线程运行时才会执行它
  • 守护线程通常用于在同一程序里给普通线程(也叫使用者线程)提供服务

差异:当JVM停止时,所有守护线程都会被抛弃,不做任务处理。

终结器

当垃圾回收器对内存进行处理后,其他的外部资源也应该被处理,例如文件句柄和套接字句柄,必须显性的归还给系统,调用 finalize主要负责这项工作。

终结器访问的任务状态都可能被其他线程访问,所以需要有同步策略,而且还会在对象上产生巨大的性能开销。 大多数情况下,请使用 finally 和 close 替代。

参考网站

线程管理(七)守护线程的创建和运行