总结Java多线程之InterruptedException

别让你的InterruptedException被中断

以前在写C#的时候,想要让线程暂停,就直接Thread.Sleep(xxx)OK,但是在Java中,你得这样写:

try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

//或者

try {
    TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
    e.printStackTrace();
}

sleep一次就得try-catch一次,实在太繁琐了。


相信大多人在最开始接触InterruptedException,对它的处理方式就是记录日志或则简单打印出来。然而,这是最错误的方法。

之所以会这么处理,是因为没有去了解InterruptedException的作用是什么,观察JDK中会抛出这个受检异常的大概有:

  • Threead#wait()
  • Threead#sleep()
  • Threead#join()
  • BlockingQueue#take()/put()
  • Lock#lockInteruptibly()
  • AQS

他们的共同点就是阻塞,当执行这些方法后,线程状态就会由RUNABLE转变为WATING状态,然而在某些情况下,由于外界条件发生变化,比如用户觉得等太久了,不想再等了,那怎么将程序唤醒并接受这种阻塞状态呢?

答案就是InterruptedException


InterruptedException 是什么?

Java中线程间协作及中断是通过interrupt协议来实现的,当某个线程需要中断另外一个线程操作的时候,会将另外一个线程的interrupt变量置位ture同时会唤醒此线程,至于是否响应中断,不同的方法有不同的处理,比如synchronized就会继续阻塞而不会响应中断。

一般常规的响应中断的方法,在检测到中断信号后,便会抛出InterruptedException异常,来通知其他方法该线程被中断,因此InterruptedException和其他异常不同的是,它算是一个信号,而不是真正的异常。

InterruptedException 怎么工作?

一般来说,想要将一个线程中断,需要调用以下方法

  • Thread#interrupt()

该方法具有两个作用:

  • interrupt变量置位true
  • 如果线程被阻塞,则唤醒线程

因此,如果能够响应中断的方法,则需要不断的检测interput变量:

//JDK1.8 CountDownLatch#await()源码
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        //如果线程被中断,则抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

可以看到,当CountDownLatch在阻塞前会检查中断变量,当需要中断的时候,就会直接抛出异常。

然而很多时候,需要中断的时候可能方法已经被阻塞了,因此interrupt还有一个作用便是唤醒线程

//JDK1.8 AbstractQueuedSynchronizer#await()源码
private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //省略部分代码
        try {
            for (;;) {
                //省略部分代码
                if (shouldParkAfterFailedAcquire(p, node) &&
                    //检查中断标志
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

可以看到,上面的parkAndCheckInterrupt()便是检查中断方法

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

一般来说,需要响应中断的方法,都应该放在一个循环中,防止被因为中断而被唤醒其他地方唤醒,并且循环中应该包含检查中断的逻辑。

需要知道的是,大多数地方检查中断调用的是Thread.interrupted()而不是Thread.isInterrupted(),他们之间的区别在于Thread.interrupted()会将interrupt标志位设置为false也就是说在方法抛出InterruptedException,再次调用isInterrupted会返回false,这就是为什么吞掉InterruptedException是错误的操作。


InterruptedException 怎么使用?

明白了InterruptedException的作用,我们就知道,接收到该异常后,直接打印到日志里面,是错误的做法。很多时候这并没有起到中断真正的作用。比如某些时候,需要将中断放在一个循环当中,那么你会发现中断无法被中断:

    public static void main(String[] args){
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.submit(() -> {
             //第一段业务
            try {
                TimeUnit.MINUTES.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //第二段业务
            try {
                TimeUnit.MINUTES.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        executorService.shutdownNow();
    }

你会发现shutdownNow()只会让第一段业务结束,但是会马上卡在第二段业务代码中,也就是任务没有被成功取消。

查看源码你会发现,Executor#shutdownNow()/shutdown()以及FutureTask#cancel()的任务取消都是基于中断实现的。

上面的代码之所以有问题就在于第一段业务逻辑直接将中断信号给“吞”了,导致第二个sleep并不知道线程已经中断。

进而我们可以知道,如果不知道具体的业务逻辑的话,可以在捕获到InterruptedException恢复中断变量。

 public static void main(String[] args){
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.submit(() -> {

            //第一段业务
            try {
                TimeUnit.MINUTES.sleep(1);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }

            //第二段业务
            try {
                TimeUnit.MINUTES.sleep(1);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }

        });

        executorService.shutdownNow();
    }

InterruptedException是作为Java中用于线程间协作的中断