7取消与关闭

概念

  • 中断(Interruption),是一种协作机制,能够使一个线程终止另一个线程的当前工作。
  • 中断只适合用于取消操作,如果用在取消之外的其他操作则是不合适的;中断是实现取消的最合理方式。

中断是必要的

  • 我们很少希望某个任务、线程或服务立即停止,因为这样会使共享的数据结构处于不一致的状态;而使用中断这种协作机制,当需要停止时,任务首先会清除当前正在执行的工作,然后再结束,因为任务本身的代码比发出请求的代码更清楚如何执行清除工作。

行为好坏的程序的区别

  • 行为良好的程序能很完善地处理失败、关闭和取消等情况。

取消任务概念

  • 外部代码能在某个任务正常完成之前将其置为“完成”状态

任务取消原因

  • 用户请求取消(例如:用户点击取消按钮)
  • 有时间限制的操作(例如:任务执行超时,则取消任务)
  • 应用程序事件(例如:一个任务的完成事件,导致其他任务取消)
  • 错误(例如:一个任务发生错误,导致其他任务取消)
  • 关闭(例如:程序运行结束,取消长运行的任务)

任务取消策略

  • How:外部代码如何请求取消任务
  • When:任务在何时检查外部代码是否已经请求了取消
  • What:任务取消时应该执行哪些操作

线程中断策略

  • 中断策略规定线程如何解释某个中断请求
  • 最合理的中断策略是某种线程级或服务级取消操作:
    • 尽快退出或抛出InterruptException异常
    • 在必要的时候进行清理(可以在线程中断后进行清理工作,例如shutdown方法内清理)
    • 通知某个所有者该线程已经退出(标记被中断线程的状态为中断)
  • 其他常用中断策略:停止服务、重新开始服务,对于包含这些非标准中断策略的线程或线程池,只能应用于知道这些策略的任务中。

注:正如任务中应该包含取消策略,线程同样应该包含中断策略

Thread中断相关操作

  • interrupt: 用于中断目标线程
  • isInterrupted: 用于检测目标线程是否会中断
  • Thread.interrupted: 用于清除当前线程的中断状态,并返回之前的中断状态,这是清除中断状态的唯一方法

注1:如果目标线程已经结束,则 isInterrupted 始终返回false!

注2:调用interrupt并不意味着立即停止目标线程正在执行的任务,而只是传递了请求中断信息!

支持中断的阻塞库方法

  • Thread.sleep
  • Thread.join
  • Object.wait

注:它们响应中断的操作包括:清除中断状态,抛出InterruptedException。也就是说,收到InterruptedException时,中断状态已经为 false !

中断请求的接收者

  • 任务和线程都是中断请求的接收者,一个中断请求到达时,意味着需要“取消任务”和“关闭工作者线程”
  • 任务不会在自己拥有的线程中运行(任务只是任务,不是线程),任务是非线程所有者,在处理中断请求时应该小心的保存中断状态,这样线程拥有者才能对中断做出响应,即使其他后续“非拥有者”也可以做出响应。

任务响应中断的两种方式

  • 传递异常InterruptedException
  • 恢复中断状态(调用Thread.interrupt方法)

注1:任务不应该对执行该任务的线程的中断策略做出任何假设,除非该任务被专门设计为服务中运行,并且在这些服务中包含特定的中断策略!

注2:只有实现了线程中断策略的代码才可以屏蔽中断请求,在常规的任务和库代码中都不应该屏蔽中断请求!

如何中断线程

  • 线程只能由其所有者中断,所有者可以将线程的中断策略信息封装到某个合适的取消机制中,里如果关闭(shutdown)方法。

任务对中断的响应

  • 支持取消且调用了中断阻塞方法的任务,应该尽快取消任务,恢复中断状态
  • 不支持取消但在循环中调用中断阻塞方法的任务,应该在本地保存中断状态,并在任务返回前恢复中断状态,而不是在捕获中断时就恢复,那样容易引起死循环
  • 不调用中断阻塞方法的任务,在循环中轮询当前线程的中断状态来响应中断

Java中断机制的优点

  • Java中断为非抢占式中断,通过推迟中断请求的处理,开发人员能指定灵活的终端策略,从而使应用程序在响应性和建壮性之间实现合理的平衡。

标准取消操作示例

package cn.weicm.cancel;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

/**
 * <b>Time:&#9;2018/8/3 13:52</b><br/>
 * <b>Auth:&#9;weicm</b><br/>
 * <br/>
 * <b>Desp:&#9;在外部线程中安排中断</b><br/>
 * <br/>
 * <b>优点:</b><br/>
 * <ul>
 *     <li>能从任务中抛出未检查异常,异常会被timedRun的调用者捕获</li>
 * </ul>
 * <br/>
 * <b>缺点:</b><br/>
 * <ul>
 *     <li>在中断线程时,不了解执行任务的线程的中断策略,因为timedRun可能在任何线程中运行</li>
 *     <li>如果任务不响应中断,那么timedRun会在任务结束时才返回,此时可能已经超过了指定时限,这会对调用者带来负面影响</li>
 * </ul>
 */
public class TimedRun1 {
    private static final ScheduledExecutorService ses = Executors.newScheduledThreadPool(1);

    public static void main(String[] args) {
        try {
            timedRun(() -> {
                System.out.println("Task start ...");
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    System.out.println("Task is canceled!");
                    //假设任务不响应中断,即不抛出InterruptedException,也不通过Thread.interrupt()恢复中断状态,而是直接退出
                    return;
                }
                System.out.println("Task end ...");
            }, 1, TimeUnit.SECONDS);
        } finally {
            ses.shutdown();
        }

    }


    /**
     * <b>Time:&#9;2018/8/3 14:20</b><br/>
     * <b>Auth:&#9;weicm</b><br/>
     * <br/>
     * <b>Desp:&#9;在制定时限内运行任务,超过时限则取消任务</b><br/>
     *
     * @param task 任务
     * @param timeout 时限数量
     * @param unit 时限单位
     */
    static void timedRun(Runnable task, long timeout, TimeUnit unit) {
        //获取目标任务所在线程,此时该线程可能是任意线程
        Thread currentThread = Thread.currentThread();
        //启动中断任务
        ScheduledFuture<?> future = ses.schedule(() -> {
            currentThread.interrupt();
        }, timeout, unit);
        task.run();
        //任务结束后,取消掉中断任务,因为此时目标任务已经结束,中断任务已经没有存在的意义了
        future.cancel(true);
    }
}
package cn.weicm.cancel;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

/**
 * <b>Time:&#9;2018/8/3 15:36</b><br/>
 * <b>Auth:&#9;weicm</b><br/>
 * <br/>
 * <b>Desp:&#9;在专门的线程中中断任务</b><br/>
 * </br>
 * <b>优点:&#9;</b><br/>
 * <ul>
 *     <li>解决了TimedRun1的所有缺点</li>
 * </ul>
 * </br>
 * <b>缺点:&#9;</b><br/>
 * <ul>
 *     <li>由于join的不足,无法知道任务是因为线程正常退出而返回还是因为join超时而返回</li>
 * </ul>
 */
public class TimedRun2 {
    private static final ScheduledExecutorService ses = Executors.newScheduledThreadPool(1);

    public static void main(String[] args) throws Throwable {
        try {
            timedRun(() -> {
                System.out.println("Task start ...");
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    System.out.println("Task is canceled!");
                    //假设任务不响应中断,即不抛出InterruptedException,也不通过Thread.interrupt()恢复中断状态,而是直接退出
                    return;
                }
                System.out.println("Task end ...");
            }, 1, TimeUnit.SECONDS);
        } finally {
            ses.shutdown();
        }
    }

    /**
     * <b>Time:&#9;2018/8/3 14:20</b><br/>
     * <b>Auth:&#9;weicm</b><br/>
     * <br/>
     * <b>Desp:&#9;在制定时限内运行任务,超过时限则取消任务</b><br/>
     *
     * @param task 任务
     * @param timeout 时限数量
     * @param unit 时限单位
     */
    static void timedRun(Runnable task, long timeout, TimeUnit unit) throws Throwable {
        /**
         * <b>Time:&#9;2018/8/3 14:42</b><br/>
         * <b>Auth:&#9;weicm</b><br/>
         * <br/>
         * <b>Desp:&#9;装执行线程的中断策略</b><br/>
         */
        class ThrowableTask implements Runnable {
            private volatile Throwable e;

            @Override
            public void run() {
                try {
                    task.run();
                } catch (Throwable e) {
                    //中断策略: 录任务的运行时异常,以便稍后重新抛出该异常,并结束执行线程
                    this.e = e;
                }
            }

            /**
             * <b>Time:&#9;2018/8/3 15:33</b><br/>
             * <b>Auth:&#9;weicm</b><br/>
             * <br/>
             * <b>Desp:&#9;重新抛出目标任务运行过程中可能发生的异常</b><br/>
             *
             * @throws Throwable
             */
            public void rethrow() throws Throwable {
                if (null != e)
                    throw e;
            }
        }
        //将目标任务运行在明确中断策略的执行线程里
        ThrowableTask t = new ThrowableTask();
        Thread taskThread = new Thread(t);
        taskThread.start();
        //启动中断任务
        ScheduledFuture<?> future = ses.schedule(() -> {
            taskThread.interrupt();
        }, timeout, unit);
        taskThread.join(unit.toMillis(timeout));
        //任务结束后,取消掉中断任务,因为此时目标任务已经结束,中断任务已经没有存在的意义了
        future.cancel(true);
        //重新抛出任务执行过程中发生的异常
        t.rethrow();
    }
}
package cn.weicm.cancel;

import java.util.concurrent.*;

/**
 * <b>Time:&#9;2018/8/3 16:38</b><br/>
 * <b>Auth:&#9;weicm</b><br/>
 * <br/>
 * <b>Desp:&#9;通过Future来取消任务</b><br/>
 * </br>
 * <b>优点:&#9;</b><br/>
 * <ul>
 *     <li>解决TimedRun2的缺点,可以区分任务是如何结束的</li>
 * </ul>
 * </br>
 * <b>关于Futrue.cancel:&#9;针对任务的三种状态</b><br/>
 * <ul>
 *     <li>等待状态:此时不管参数传入的是true还是false,任务都会被标记为取消,任务依然保存在队列中,但当轮询到此任务时会直接跳过</li>
 *     <li>运行状态:此时参数传入true会中断正在执行的任务;传入false则不会中断任务,而是让任务继续运行直到结束</li>
 *     <li>完成状态:此时不管参数传入的是true还是false,cancel都不起作用,因为任务已经完成了</li>
 * </ul>
 */
public class TimedRun3 {
    private static final ExecutorService es = Executors.newSingleThreadExecutor();
    public static void main(String[] args) throws Exception{
        try {
            timedRun(() -> {
                System.out.println("Task start ...");
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    System.out.println("Task is canceled!");
                    //假设任务不响应中断,即不抛出InterruptedException,也不通过Thread.interrupt()恢复中断状态,而是直接退出
                    return;
                }
                System.out.println("Task end ...");
            }, 1, TimeUnit.SECONDS);
        } finally {
            es.shutdown();
        }
    }

    /**
     * <b>Time:&#9;2018/8/3 14:20</b><br/>
     * <b>Auth:&#9;weicm</b><br/>
     * <br/>
     * <b>Desp:&#9;在制定时限内运行任务,超过时限则取消任务</b><br/>
     *
     * @param task 任务
     * @param timeout 时限数量
     * @param unit 时限单位
     */
    static void timedRun(Runnable task, long timeout, TimeUnit unit) throws Exception {
        Future<?> future = es.submit(task);
        try {
            future.get(timeout, unit);
        } catch (InterruptedException e) {
            //当前线程被中断,恢复中断状态以传递中断信息
            Thread.currentThread().interrupt();
        } catch (ExecutionException e) {
            //目标任务执行过程发生运行时异常,直接抛出
            throw e;
        } catch (TimeoutException e) {
            //目标任务执行超时,接下来取消任务,因为已经不需要结果了
        } finally {
            //如果任务已经运行完,则取消操作不产生任何效果;如果任务由与异常而终止,不管什么异常,则取消任务,因为已经不需要结果了
            //取消那些不在需要结果的任务是一种良好的习惯!
            future.cancel(true);
        }
    }
}

不可中断的阻塞

  • Thread.interrupt对于执行不可中断的操作而阻塞的线程,只能设置线程的中断状态,除此之外没有其他任何作用;但可以使用类似于中断的手段来停止这些线程,但是必须知道线程阻塞的原因。

常见不可中断的阻塞操作

  • Java.io包中的同步Socket I/O: InputStream.read和OutputStream.write,可以关闭套接字(Socket.close)使他们抛出SocketException。
  • Java.io包中的同步I/O: 当中断一个正在InterruptibleChannel上等待的线程时,将抛出ClosedByInterruptException并关闭链路,这还会使得其他在该链路上阻塞的线程同样抛出ClosedByInterruptException。当关闭一个InterruptibleChannel时,将导致所有在链路操作上阻塞的线程都抛出AsynchronousCloseException。
  • Selector的异步I/O: 如果一个线程在调用Selector.select方法时阻塞了,那么调用close或wakeup方法会使线程抛出ClosedSelectorException并提前返回。
  • 获取某个锁: 如果一个线程由与等待某个内置锁而阻塞,那么将无法响应中断,因为线程认为它肯定会获得锁,所以将不会理会中断请求。但是Lock类中提供了lockInterruptiblely方法,该方法允许在等待一个锁的同时仍能响应中断。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容