每日一博 - 闲聊 Java 中的中断

2023-09-30 08:19:20 浏览数 (1)


概述

在 Java 中,中断是一种线程协作方式 。

比如说,当线程 A 正在运行时,线程 B 可以通过中断线程 A,来指示线程 A 停止它正在执行的操作。但是线程 A 如何响应线程 B 的中断,是需要依靠线程 A 的代码处理逻辑来做决定的。


常见的中断问题

  • 首先,我们来看一下如何来中断一个线程,以及如何判断一个线程是否被中断了。
  • 接着,我们看下中断处于不同状态下的线程时,被中断的线程会做如何响应。 然后,我们学习如何正确地利用中断标识来处理中断
  • 最后,我们看一下 JDK 的线程池 ThreadPoolExecutor 内部是如何运用中断实现功能的

中断一个处于运行状态的线程

我们先来看下,如何中断一个线程,以及中断一个处于运行状态的线程后,这个线程会做出什么反应呢?

代码语言:javascript复制
public class InterruptDemo {
  public static void main(String[] argc) throws InterruptedException {
    //1. 创建子线程
    Thread threadOne = new Thread(new Runnable() {
      @Override
      public void run() {
        for (; ; ) {
          System.out.println("im threadOne thread:"   Thread.currentThread().isInterrupted());
        }
      }
    }, "THREAD-ONE");
    //2.启动线程
    threadOne.start();
    //3. main线程休眠1s
    Thread.sleep(1000);
    //4. 中断子线程
    threadOne.interrupt();
  }
}

在这段代码中,我们首先创建了一个名为“THREAD-ONE”的线程。线程所做的事情很简单,就是打印一行文本。然后,我们启动这个线程。

接着,我们让 main 线程休眠 1s,这是为了让创建的子线程可以在被中断前,可以打印子线程的中断标识。 最后,我们调用子线程的 interrupt() 方法来中断子线程。

运行这段代码会发现,在代码 4 执行之前,子线程会一直输出:im threadOne thread:false。代码 4 执行完毕后,子线程会一直运行,并且一直输出:im threadOne thread:true

简单来说,我们可以通过调用线程的 interrupt() 方法来中断某个线程。中断处于运行状态的线程并不会对它造成影响,中断线程仅仅是把被中断的线程的标志设置为了 true。另外,我们可以调用线程的 isInterrupted() 方法,来判断线程是否被中断了


中断一个正在 sleep 的线程

中断处于运行状态的线程不会有影响,那中断一个正在 sleep 的线程,会对这个线程产生什么影响呢?我们再来看一个案例

代码语言:javascript复制
public class InterruptSleepDemo {
  public static void main(String[] argc) throws InterruptedException {
    //1. 创建子线程
    Thread threadOne = new Thread(new Runnable() {
      @Override
      public void run() {
        //1.1
        System.out.println("sub thread begin run");
        try {
          //1.2
          Thread.sleep(1000);
        } catch (InterruptedException e) {
         //1.3
          e.printStackTrace();
        }
        //1.4
        System.out.println("sub thread end run");
      }
    }, "THREAD-ONE");
    //2.启动线程
    threadOne.start();
    //3. main线程休眠100ms
    Thread.sleep(100);
    //4. 中断子线程
    threadOne.interrupt();
    System.out.println("threadOne already interrupted");
  }
}

我们先创建一个名称为“THREAD-ONE”线程,这个线程内部调用 Thread.sleep(1000) 来让自己休眠 1s,然后启动这个线程,开始运行。

接着,我们让 main 线程休眠 100ms,为的是在 main 线程执行代码 4 前,先让子线程执行代码 1.2 Thread.sleep(1000),并让子线程处于 TIMED_WAITING 状态。代码 4 则会调用子线程的 interrupt() 方法,来中断子线程。

运行上面这段代码,我们会看到这样的运行结果:

代码语言:javascript复制
java.lang.InterruptedException: sleep interrupted
  at java.lang.Thread.sleep(Native Method)
  at org.example.InterruptSleepDemo$1.run(InterruptSleepDemo.java:12)
  at java.lang.Thread.run(Thread.java:748)

可以看到,当 main 线程执行完代码 4,中断了子线程后,子线程会在代码 1.2 的地方抛出 InterruptedException 异常。然后,代码 1.3 会捕获到这个异常并打印异常信息,最后执行代码 1.4,并退出线程的执行,这时线程就处于终止状态了。

总的来说,中断一个由于调用 Thread.sleep() 方法而处于 TIMED_WAITING 状态的线程,会导致被中断的线程抛出 InterruptedException 异常


中断一个由于获取 ReentrantLock 锁而被阻塞的线程

当中断一个由于获取 ReentrantLock 锁而被阻塞的线程,会产生什么效果呢?我们来看一下这段代码示例:

代码语言:javascript复制
public class InterruptLockDemo {

  //0.创建独占锁
  private final static ReentrantLock LOCK = new ReentrantLock();

  public static void main(String[] argc) throws InterruptedException {
    //1. 创建子线程
    Thread threadOne = new Thread(new Runnable() {
      @Override
      public void run() {
        //1.1
        System.out.println("sub thread begin run");
        try {
          //1.2
          LOCK.lockInterruptibly();
          System.out.println("sub thread got lock");
        } catch (InterruptedException e) {
          //1.3
          e.printStackTrace();
        } finally {
          //1.4
          LOCK.unlock();
        }
        System.out.println("sub thread end run");
      }
    }, "THREAD-ONE");
    //2. main线程获取锁
    LOCK.lock();
    //3.启动线程
    threadOne.start();
    //4. main线程休眠100ms
    Thread.sleep(100);
    //5. 中断子线程
    threadOne.interrupt();
    //6. main线程释放锁
    LOCK.unlock();
  }
}

我们先创建一个独占锁,再创建一个名为“THREAD-ONE”的子线程,然后让 main 线程获取到这个独占锁,启动并运行子线程。让 main 线程休眠 100ms,是为了保证代码 1.2 的执行发生在中断子线程之前。

子线程执行到代码 1.2 时,发现锁已经被其他线程持有了,就会处于阻塞状态。当 main 线程执行到中断子线程代码时,子线程就会从阻塞状态返回,然后抛出 InterruptedException 异常。

运行上面这段代码,我们会得到如下所示的结果:

代码语言:javascript复制
java.lang.InterruptedException
  at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
  at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
  at org.example.InterruptLockDemo$1.run(InterruptLockDemo.java:18)
  at java.lang.Thread.run(Thread.java:748)
Exception in thread "THREAD-ONE" java.lang.IllegalMonitorStateException
  at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
  at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
  at org.example.InterruptLockDemo$1.run(InterruptLockDemo.java:25)
  at java.lang.Thread.run(Thread.java:748)

综上所述,当中断一个由于使用 lockInterruptibly() 方法获取锁而阻塞的线程时,这个线程会从阻塞状态返回,然后会抛出 InterruptedException 异常


如何正确地使用线程的中断标识

说完这几种常见线程的中断案例,我们再来看一看,如何正确地使用线程的中断标识,来让被中断的线程正常退出执行呢

代码语言:javascript复制
public class UseInterruptDemo {
  public static void main(String[] argc) throws InterruptedException {
    //1. 创建子线程
    Thread threadOne = new Thread(new Runnable() {
      @Override
      public void run() {
       // 1.1
        for (; !Thread.currentThread().isInterrupted(); ) {
          System.out.println("---do something");
        }
      }
    }, "THREAD-ONE");
    //2.启动线程
    threadOne.start();
    //3. main线程休眠1s
    Thread.sleep(1000);
    //4. 中断子线程
    threadOne.interrupt();
    System.out.println("threadOne already interrupted");
  }
}

在这段代码中,我们先创建一个线程,然后,启动并运行这个线程。让 main 线程休眠 1s,为的是让子线程的代码 1.1 可以打印数据,

接下来,调用子线程的 interrupt() 方法中断子线程。这时,子线程代码 1.1 的循环语句判断自己被中断了,就退出循环的执行,子线程也就结束运行了。

所以,中断处于运行状态的线程时,我们可以在被中断的线程内部判断当前线程的中断标识位是否被设置了,如果被设置了,就退出代码的执行,然后被中断的线程也就可以优雅地退出执行了


JDK 的线程池 ThreadPoolExecutor 内部是如何运用中断实现功能的

我们使用 ThreadPoolExecutor,在程序运行结束时,我们会调用它的 shutdown() 方法来关闭线程池。关闭线程池的其中一个步骤,就是中断当前不活跃的工作线程。

代码语言:javascript复制
public void shutdown() {
     final ReentrantLock mainLock = this.mainLock;
     mainLock.lock();
     try {
         ...
         interruptIdleWorkers();
         ...
     } finally {
         mainLock.unlock();
     }
     tryTerminate();
 }
 private void interruptIdleWorkers(boolean onlyOne) {
     final ReentrantLock mainLock = this.mainLock;
     mainLock.lock();
     try {
         for (Worker w : workers) {
             Thread t = w.thread;
             if (!t.isInterrupted() && w.tryLock()) {
                 try {
                     t.interrupt(); // 中断线程
                 } catch (SecurityException ignore) {
                 } finally {
                     w.unlock();
                 }
             }
             if (onlyOne)
                 break;
         }
     } finally {
         mainLock.unlock();
     }
 } 

上面这段代码中,shutdown 方法内会调用 interruptIdleWorkers() 方法,来中断线程池中的空闲线程,interruptIdleWorkers() 内部会通过循环遍历所有的 Worker 线程,并且如果当前线程没被中断,则会中断当前线程。

那么,中断空闲线程会产生什么效果呢?我们需要看下线程池中工作线程的处理逻辑。

代码语言:javascript复制
final void runWorker(Worker w) {
     ...
     try {
         // 如果获取不到任务,则退出循环
         while (task != null || (task = getTask()) != null) {
           ....
         }
         completedAbruptly = false;
     } finally {
         processWorkerExit(w, completedAbruptly);
     }
 }
 private Runnable getTask() {
     ...
     for (;;) {
         ...
         if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
             decrementWorkerCount();
             return null;
         }
         ...
         try {
             //
             Runnable r = timed ?
                 workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                 workQueue.take();
             if (r != null)
                 return r;
             timedOut = true;
         } catch (InterruptedException retry) {
             timedOut = false;
         }
     }
 }

这段代码,在 runWorker() 方法内,工作线程会通过循环从线程池队列里面获取任务,如果获取到任务,工作线程就进行处理;如果获取不到,就退出执行。

这个 getTask() 方法是负责从线程池队列里面获取任务的。默认情况下,getTask() 方法会执行 workQueue.take(),从队列里面获取任务。如果当前队列没有任务,这个方法会阻塞,也就是这个工作线程就会被阻塞。

当其他线程调用线程池的 shutDown() 方法时,会中断阻塞到 workQueue.take() 方法的工作线程,然后这个工作线程就会从阻塞中返回,并抛出 InterruptedException 异常

异常被捕获后,getTask() 方法继续执行 for 循环,接着发现线程池已经关闭了,getTask() 就会返回 null。到此为止,当前工作线程就执行完毕了,就会被释放掉


小结

Java 中每个线程都有一个中断标识,用来标识当前线程是否被中断了。我们可以通过调用线程的 interrupt() 方法来中断一个线程,一个线程被中断后,它的中断标识就被设置为了 true,我们可以通过调用线程的 isInterrupted() 方法来判断这个线程是否被中断。

当我们中断一个处于运行状态的线程,比如线程正在执行计算,这时仅仅是把线程的中断标识设置为了 true,并不会对计算任务造成影响。

还有一类线程,因为调用了 Object 类的 wait()、wait(long) 或 wait(long, int) 方法或者 Thread 的 join()、join(long)、join(long, int),sleep(long)、 sleep(long, int) 方法而被阻塞。当我们中断这类线程时,被阻塞的线程会从阻塞状态返回,并抛出 InterruptedException 异常。

0 人点赞