JAVA并发体系-1.1-终结任务或线程

中断-interrupt

本文讨论的问题是:在某些情况下,任务必须突然的终止。即有时你希望能够终止处于阻塞状态的任务(todo: 关于何时处于堵塞请查看线程和任务中的解释)。如果对于处于阻塞状态的任务,你不能等待其到达代码中可以检查其状态值的某一点,因而决定让它主动地终止,那么你就必须强制这个任务跳出阻塞状态。

Runnable.run()方法的中间打断任务,与等待任务执行方法到达检查cancel标志的语句,或者到达程序员准备好离开该执行方法的其他一些地方相比,要棘手得多。

当打断被阻塞的任务时,可能需要清理资源。正因为这一点,在任务的run()方法中间打断,更像是抛出的异常,因此在Java线程中的这种类型的异常中断中用到了异常。虽然,异常从来都不能异步地传递;但是只要在使用对象互斥机制(与 synchronized关键字相对)时使用try- finally惯用法,在指令/方法调用的中间突然中断没有任何危险。如果抛出异常,使用try- finally惯用法这些互斥就会自动被释放。

Thread上的中断

Thread类包含 interrupt()方法,可以使用它终止被阻塞的任务。 interrupt()方法将设置线程的中断状态。

  • 调用时刻:如果一个线程已经被阻塞,或者试图执行一个阻塞操作,这时设置这个线程的中断状态将抛出 InterruptedException

    • 中断发生的唯一时刻是在任务要进入到阻塞操作中,或者已经在阻塞操作内部时
  • 调用过程:当另一个a线程在该b线程上调用 interrupt()方法时,a线程将给该b线程设定一个标志,表明该b线程已经被中断。

  • 重置标志位:

    • InterruptedException异常被捕获时(即抛出该异常时)将清理interrupt标志,中断状态将被复位。所以在 catch子句中,在异常被捕获的时候interrupt标志总是为假。
    • 当任务调用 Thread.isInterrupted()检查中断状态时,中断状态将被复位。
    • 如果想要再次检查以了解是否被中断,则可以在调用 Thread.isInterrupted()时将结果存储起来。
  • 调用 interrupt,必须持有 Thread对象。

Executor上的中断操作

新的concurrent类库在避免对Thread对象的直接操作,转而尽量通过 Executor来执行所有操作。但是正如上面所说"调用 interrupt,必须持有 Thread对象。",我们需要知道如何通过 Executor操作。(注意:ExecutorService继承了Executor接口)

  • 中断Executor上的所有进程:

    在ExecutorService上调用 shutdownNow(),那么它将发送一个 interrupt()调用给它启动的所有线程。(这么做是有意义的,因为当你完成工程中的某个部分或者整个程序时,通常会希望同时关闭某个特定 Executor的所有任务)

  • 中断Executor上的单个线程:

    通过调用 submit()来启动任务,持有该任务的上下文即Future。持有这种 Future的关键在于你可以在其上调用 cancel(),并因此可以使用它来中断某个特定任务,将true传递给 cancel(),那么它就会拥有在该线程上调用 interrupt以停止这个线程的权限。

    ExecutorService exec = Executors.newCachedThreadPool();
    Future<?> f = exec.submit(new Runnable(){...});
    f.cancel(true); // Interrupts if running
    

能/不能中断的调用

interrupt()能够中断的调用:

  • 对 sleep()的调用
  • 任何要求抛出 InterruptedException的调用
    • 例如在ReentrantLock上堵塞的任务
  • 打断被互斥所堵塞的调用

interrupt()不能中断的调用:

  • 正在试图获取 synchronized锁的任务
  • 试图执行IO操作的任务

存在不能中断的调用的事实有点令人烦恼,特别是在创建执行IO的任务时。因为这意味着IO具有锁住你的多线程程序的潜在可能。特别是对于基于Web的程序,这更是关乎利害。

对于这类问题,有一个略显笨拙但是有时确实行之有效的解决方案,即关闭任务在其上发生阻塞的底层资源

ExecutorService exec = Executors.newCachedThreadPool();
ServerSocket server = new ServerSocket(8080);
exec.execute(new IOBlocked(socketInput));
exec.shutdownNow(); //this will not shutdown
socketInput.close(); // Releases blocked thread and then the thread will shutdown

一个ReentrantLock上中断的例子

ReentrantLock上被堵塞的任务被中断的例子如下代码所示,在这个代码中本输出的执行流程为:point 1>> point 2>>point 3>>point 4>>point 5>>point 6>>point 7>>point 8>>point 9

关于这个例子需要注意:尽管不太可能,但是对t.interrupt()的调用确实可以发生在对blocked.f()的调用之前,进而产生死锁。

class BlockedMutex {
  private Lock lock = new ReentrantLock();
  public BlockedMutex() {
    // Acquire it right away, to demonstrate interruption
    // of a task blocked on a ReentrantLock:
    lock.lock();
  }
  public void f() {
    try {
      // This will never be available to a second task
      lock.lockInterruptibly(); // Special call  // point 4 
      print("lock acquired in f()");
    } catch(InterruptedException e) {// point  8
      print("Interrupted from lock acquisition in f()");
    }
  }
}

class Blocked2 implements Runnable {
  BlockedMutex blocked = new BlockedMutex();
  public void run() {
    print("Waiting for f() in BlockedMutex");// point 2 
    blocked.f();// point 3 
    print("Broken out of blocked call"); // point 9 
  }
}

public class Interrupting2 {
  public static void main(String[] args) throws Exception {
    Thread t = new Thread(new Blocked2());
    t.start();// point 1 
    TimeUnit.SECONDS.sleep(1);// point 5 
    System.out.println("Issuing t.interrupt()");// point 6 
    t.interrupt();// point 7 
  }
}
/*Output:
*Waiting for f() in BlockedMutex
*Issuing t.interrupt()
*Interrupted from lock acquisition in f()
*Broken out of blocked call
*/

中断检查

检查中断,保证总是可以离开任务体或无限循环

如果你只能通过在阻塞调用上抛出异常来退出,那么你就无法总是可以离开run()循环。你可以采取如下两种方法来保证退出任务。

  • 可以通过调用 Thread.isInterrupted()来检查中断状态,这不仅可以告诉你interrupt()是否被调用过,而且还可以清除中断状态。

  • 可以经由单一的InterruptedException或单一的成功的 Thread.interrupted()测试来得到这种通知。

虽然中断状态在上面两种方法下会被清除,但正如上文所说的:如果想要再次检查以了解是否被中断,则可以在调用 Thread.isInterrupted()时将结果存储起来。

中断任务的示范

策略:通过学习之后的这个代码示例来保证可以退出任务体或无限循环)在下面这个例子中,你可以在不同地点退出 Blocked3.run():在阻塞的 sleep()调用中,或者在非阻塞的数学计算中。你将看到,如果 interrupt(在注释 point2之后(即在非阻塞的操作过程中)被调用,那么首先循环将结束,然后所有的本地对象将被销毁,最后循环会经由 while语句的顶部退出。但是,如果 interrupt()在 point1和 point2之间(在 while语句之后,但是在阻塞操作 sleep()之前或其过程中)被调用,那么这个任务就会在第一次试图调用阻塞操作之前,经由InterruptedException退出。在这种情况下,在异常被抛出之时唯一被创建出来的 NeedsCleanup对象将被清除,而你也就有了在catch子句中执行其他任何清除工作的机会。

注意:被设计用来响应 interrupt的类必须建立一种策略,来确保它将保持一致的状态。这通常意味着所有需要清理的对象创建操作的后面,都必须紧跟 try-finally子句,从而使得无论run()循环如何退出,清理都会发生。

class NeedsCleanup {
  private final int id;
  public NeedsCleanup(int ident) {
    id = ident;
    print("NeedsCleanup " + id);
  }
  public void cleanup() {
    print("Cleaning up " + id);
  }
}

class Blocked3 implements Runnable {
  private volatile double d = 0.0;
  public void run() {
    try {
      while(!Thread.interrupted()) {
        // point1
        NeedsCleanup n1 = new NeedsCleanup(1);
        // Start try-finally immediately after definition
        // of n1, to guarantee proper cleanup of n1:
        try {
          print("Sleeping");
          TimeUnit.SECONDS.sleep(1);
          // point2
          NeedsCleanup n2 = new NeedsCleanup(2);
          // Guarantee proper cleanup of n2:
          try {
            print("Calculating");
            // A time-consuming, non-blocking operation:
            for(int i = 1; i < 2500000; i++)
              d = d + (Math.PI + Math.E) / d;
            print("Finished time-consuming operation");
          } finally {
            n2.cleanup();
          }
        } finally {
          n1.cleanup();
        }
      }
      print("Exiting via while() test");
    } catch(InterruptedException e) {
      print("Exiting via InterruptedException");
    }
  }
}

public class InterruptingIdiom {
  public static void main(String[] args) throws Exception {
    if(args.length != 1) {
      print("usage: java InterruptingIdiom delay-in-mS");
      System.exit(1);
    }
    Thread t = new Thread(new Blocked3());
    t.start();
    TimeUnit.MILLISECONDS.sleep(new Integer(args[0]));
    t.interrupt();
  }
} /* Output: (Sample)
NeedsCleanup 1
Sleeping
NeedsCleanup 2
Calculating
Finished time-consuming operation
Cleaning up 2
Cleaning up 1
NeedsCleanup 1
Sleeping
Cleaning up 1
Exiting via InterruptedException
*///:~

posted @ 2020-03-23 00:56  cheaptalk肥皂  阅读(115)  评论(0编辑  收藏  举报