《java并发编程实战》的第7章是任务的取消与关闭。我觉得这一章和第6章任务执行同样重要,一个在行为良好的软件和勉强运行的软件之间的最主要的区别就是,行为良好的软件能很完善的处理失败、关闭和取消等过程。

一、任务取消

  在java中没有一种安全的抢占式(收到中断请求就立刻停止)的方式来停止线程,因此也没有安全的抢占式方法来停止任务。只有一些协作式的机制,比如设置请求已取消的标识。在下面的例子中,PrimeGenarator持续的列出素数,直到它被取消。cancel方法将设置cancelled标识,并且主循环在搜索下一个素数之前会检查这个标识,为了使这个过程可靠工作,cancelled必须是volatile类型。

//使用volatile类型的域来保护取消状态
public class PrimeGenerator implements Runnable{
private final List<BigInteger> primes = new ArrayList<BigInteger>(); private volatile boolean cancelled; @Override
public void run() {
BigInteger p = BigInteger.ONE;
while (!cancelled) {
p = p.nextProbablePrime();
synchronized (this) {
primes.add(p);
}
}
} public void cancel(){cancelled = true;} public synchronized List<BigInteger> get(){
return new ArrayList<BigInteger>(primes);
}
}

测试,让genarator只执行一秒。

public static void main(String[] args) throws Exception{
PrimeGenerator generator = new PrimeGenerator();
new Thread(generator).start();
try {
Thread.sleep(1000);
}finally{
generator.cancel();
} System.out.println(generator.get());;
}

打印结果就不陈列了,无非就是一些素数而已。

二、中断

  每一个线程都有一个boolean类型的中断状态。当中断线程时,这个线程的中断状态将被设置为true ,在Thread中包含了中断线程以及查询线程中断状态的方法,如下:

public class Thread{
//中断目标线程(但线程不会立刻停止,也就是不会抢占式停止)
public void interrupt(){}
//返回线程的中断状态 已中断:true 未中断:false
public boolean isInterrupted(){}
//清除当前线程的中断状态,并返回它之前的值,这是清除中断状态的唯一方法
public static boolean interrupted(){}
}

  如果一个线程被中断,会发生两件事情:1.清除中断状态 2.抛出InterruptedException异常,所以,有时候如果捕获了InterruptedException后还要有其他操作的话,要把当前线程中断:Thread.currentThread().interrupt();然后再做其他事。

  还有一点需要注意,调用interrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。对中断操作的正确理解是:它并不会真正的中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻自己中断。

  现在我们回过头来看PrimeGenarator的例子,如果代码中出现了阻塞队列,那这种用volatile做标识的取消就可能会有问题,比如基于生产者和消费者模式的素数生成:

//生产者
class BrokenPrimeProducer extends Thread{ private final BlockingQueue<BigInteger> queue;
private volatile boolean cancelled = false; public BrokenPrimeProducer(BlockingQueue<BigInteger> queue) {
this.queue = queue;
} @Override
public void run() {
try {
BigInteger p = BigInteger.ONE;
while (!cancelled) {
queue.put(p = p.nextProbablePrime());
}
} catch (InterruptedException e) { }
} public void cancel(){cancelled = true;}
}

  消费者的代码就不写了,无非是从阻塞队列中取出素数。你看,这个时候,生产者线程生成素数,并将它们放入阻塞队列中,如果生产者的速度超过了消费者的处理速度,队列将被填满,put方法也会被阻塞,如果这时消费者想取消生产者这个任务就无济于事了,因为生产者阻塞在了put方法中。这个问题很好解决,使用中断来替代boolean标识。修改生产者代码:

class PrimeProducer extends Thread{

    private final BlockingQueue<BigInteger> queue;

    PrimeProducer(BlockingQueue<BigInteger> queue){
this.queue = queue;
} @Override
public void run() {
try {
BigInteger p = BigInteger.ONE;
//条件改为当前线程是否中断
while (!Thread.currentThread().isInterrupted()) {
queue.put(p = p.nextProbablePrime());
}
} catch (InterruptedException e) {
/*允许线程退出*/
}
} public void cancel(){interrupt();}
}

消费者如果不再需要生产者,可以调用cancel方法,这样即使生产者处于阻塞状态,一样可以退出。由此可见,中断是实现取消的最合理方式

三、中断策略

  最合理的中断策略是:尽快退出,并通知任务所有者该线程已经退出。因为任务一般不会在自己拥有的线程中执行,而是在比如线程池的服务中执行,对于线程池实现以外的代码,不应该对中断做过多干预。就比如当你为一户人家打扫房间时,即使主人不在,也不应该把这段时间内收到的邮件扔掉,而应该把邮件收起来,等主人回来以后再交给他们处理,尽管你可以阅读他们的杂志。这就是为什么大多数的api方法只是抛出InterruptedException作为中断响应,它们永远不会在在自己的线程中运行,因此它们为任务或者库代码实现了最合理的取消策略,也就是本段开头黑字部分,尽快退出,并把中断信息传递给调用者。另外,如果除了将InterruptedException传递给调用者外,还要执行其他操作,那么应该在捕获InterruptedException以后恢复中断状态:Thread.currentThread().interrupt();

  由于每个线程拥有各自的中断策略,因此除非你知道中断对该线程的含义,否则就不应该中断这个线程

四、用Future实现取消

  ExecutorService.submit将返回一个Future来描述一个任务,Future拥有一个cancel方法,该方法带有一个boolean类型的参数mayInterruptIfRunning,为true表示该 能被中断,为false表示若任务还没启动,就不要运行它。

  示例:执行一个任务,在一段时间内有结果则返回,没有结果则取消。伪代码如下:

public static void timedRun(Runnable r,
long timeout,TimeUnit unit) throws InterruptedException{
Future<?> task = taskExec.submit(r);
try {
task.get(timeout,unit);
} catch (TimeoutException e) {
//finally中取消
}catch (ExecutionException e) {
// 重新抛出
throw new ExecutionException(e.getCause());
}finally{
//无论任务是抛异常还是正在运行,都将结束
task.cancel(true);
}
}

这一章就到这里吧,开发中应该用到的时候也不多,希望能在看别人代码的时候有所帮助。

  

  

  

  

java并发基础(四)--- 取消与中断的更多相关文章

  1. java并发基础(二)

    <java并发编程实战>终于读完4-7章了,感触很深,但是有些东西还没有吃透,先把已经理解的整理一下.java并发基础(一)是对前3章的总结.这里总结一下第4.5章的东西. 一.java监 ...

  2. java并发基础及原理

    java并发基础知识导图   一 java线程用法 1.1 线程使用方式 1.1.1 继承Thread类 继承Thread类的方式,无返回值,且由于java不支持多继承,继承Thread类后,无法再继 ...

  3. Java 并发基础

    Java 并发基础 标签 : Java基础 线程简述 线程是进程的执行部分,用来完成一定的任务; 线程拥有自己的堆栈,程序计数器和自己的局部变量,但不拥有系统资源, 他与其他线程共享父进程的共享资源及 ...

  4. Java并发基础概念

    Java并发基础概念 线程和进程 线程和进程都能实现并发,在java编程领域,线程是实现并发的主要方式 每个进程都有独立的运行环境,内存空间.进程的通信需要通过,pipline或者socket 线程共 ...

  5. 【搞定 Java 并发面试】面试最常问的 Java 并发基础常见面试题总结!

    本文为 SnailClimb 的原创,目前已经收录自我开源的 JavaGuide 中(61.5 k Star![Java学习+面试指南] 一份涵盖大部分Java程序员所需要掌握的核心知识.欢迎 Sta ...

  6. java并发基础(五)--- 线程池的使用

    第8章介绍的是线程池的使用,直接进入正题. 一.线程饥饿死锁和饱和策略 1.线程饥饿死锁 在线程池中,如果任务依赖其他任务,那么可能产生死锁.举个极端的例子,在单线程的Executor中,如果一个任务 ...

  7. Java 并发基础知识

    一.什么是线程和进程? 进程: 是程序的一次执行过程,是系统运行程序的基本单元(就比如打开某个应用,就是开启了一个进程),因此进程是动态的.系统运行一个程序即是一个程序从创建.运行到消亡的过程. 在 ...

  8. Java并发基础框架AbstractQueuedSynchronizer初探(ReentrantLock的实现分析)

    AbstractQueuedSynchronizer是实现Java并发类库的一个基础框架,Java中的各种锁(RenentrantLock, ReentrantReadWriteLock)以及同步工具 ...

  9. 【Java并发编程四】关卡

    一.什么是关卡? 关卡类似于闭锁,它们都能阻塞一组线程,直到某些事件发生. 关卡和闭锁关键的不同在于,所有线程必须同时到达关卡点,才能继续处理.闭锁等待的是事件,关卡等待的是其他线程. 二.Cycli ...

随机推荐

  1. git 学习小记之记住https方式推送密码

    昨天刚刚学了点git基础操作,但是不幸的是Git@OSC给出公告说尽量使用 https 进行操作.可是在用 https 进行 push 时,都需要输入帐号和密码. 各种百度谷歌之后在Git@OSC官网 ...

  2. [百度地图] 用于类似 DWZ UI 框架的 百度地图 功能封装类 [MultiZMap.js] 实例源码

    MultiZMap 功能说明 MultiZMap.js 本类方法功能大多使用 prototype 原型 实现,它是 ZMap 的多加载版本,主要用于类似 DWZ 这个 多标签的 UI 的框架: 包含的 ...

  3. MAC Book 共享网络连接

    CHENYILONG Blog MAC Book 共享网络连接 MAC Book 共享网络连接 MAC比Windows共享连接要方便很多,只需要以下两步操作: 1.打开系统偏好设置,选择共享 2.选择 ...

  4. CString 与其它数据类型转换问题

    CString 头文件#include <afx.h> string 头文件#include <string.h> CString 转char * CString cstr; ...

  5. Python 入门基础5 --元组、字典、集合

    今日目录: 一.元组 二.字典 三.集合 四.后期添加内容 一.元组 1.定义 t1 = () # 参数为for可以循环的对象(可迭代对象) 思考: 如何定义一个只有一个值的元组? ("li ...

  6. Hibernate的批量查询

    Hibernate的查询大致分为以下三种场景, 1. HQL查询-hibernate Query Language(多表查询,但不复杂时使用)    2. Criteria查询(单表条件查询) 3. ...

  7. 洛谷 P4128: bzoj 1815: [SHOI2006]有色图

    题目传送门:洛谷 P4128. 计数好题,原来是 13 年前就出现了经典套路啊.这题在当年应该很难吧. 题意简述: \(n\) 个点的完全图,点没有颜色,边有 \(m\) 种颜色,问本质不同的图的数量 ...

  8. %08lx

    u-boot中代码如下: debug ("Now running in RAM - U-Boot at: %08lx\n", dest_addr); 对应设备上的打印消息如下: N ...

  9. Qt 数字和字符处理总结

    1. 四舍五入保留小数几位 QString str="12.3456789"; double d1=str.toDouble(); qDebug()<<"d1 ...

  10. linux后端诊断与调试技术

    本文不是liunx命令使用教程,也不打算全方面阐明其用法,互联网公司项目很多,服务程序之间相互依赖调用很复杂,各种因素会影响线程服务正常运行,特别是基础服务组件更是如此,当出现各种问题时,如何诊断li ...