深入浅出 Java Concurrency (36): 线程池 part 9 并发操作异常体系[转]

时间:2021-01-23 17:55:53

并发包引入的工具类很多方法都会抛出一定的异常,这些异常描述了任务在线程池中执行时发生的例外情况,而通常这些例外需要应用程序进行捕捉和处理。

例如在Future接口中有如下一个API:

 

java.util.concurrent.Future.get(long, TimeUnit) throws InterruptedException, ExecutionException, TimeoutException;

 

前面的章节中描述了Future类的具体实现原理。这里不再讨论,但是比较好奇的抛出的三个异常。

这里有一篇文章(Java 理论与实践: 处理 InterruptedException)描述了InterruptedException的来源和处理方式。简单的说就是线程在执行的过程中被自己或者别人中断了。这时候为了响应中断就需要处理当前的异常。

对于java.lang.Thread而言,InterruptedException也是一个很诡异的问题。

中断一个线程Thread.interrupt()时会触发下面一种情况:

如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException。

检测一个线程的中断状态描述是这样的Thread.interrupted():

测试当前线程是否已经中断。线程的中断状态 由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。 

也就是说如果检测到一个线程已经被中断了,那么线程的使用方(挂起、等待或者正在执行)都将应该得到一个中断异常,同时将会清除异常中断状态。

 

V innerGet(long nanosTimeout) throws InterruptedException, ExecutionException, TimeoutException {
    if (!tryAcquireSharedNanos(0, nanosTimeout))
        throw new TimeoutException();
    if (getState() == CANCELLED)
        throw new CancellationException();
    if (exception != null)
        throw new ExecutionException(exception);
    return result;
}

 

上面获取任务结果的方法实现中,将在获取锁的过程中得到一个中断异常。代码java.util.concurrent.locks.AbstractQueuedSynchronizer.tryAcquireSharedNanos(int, long)描述了这种情况:

    public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquireShared(arg) >= 0 ||
        doAcquireSharedNanos(arg, nanosTimeout);
    }

 

 

这里在获取锁的时候检测线程中断情况,如果被中断则清除中断位,同时抛出一个中断异常。为什么如此做?因为我们的线程在线程池中是被重复执行的,所以一旦线程被中断后并不会退出线程,而是设置中断位,等候任务队列自己处理线程,从而达到线程被重复利用的目的。有兴趣的可以参考代码java.util.concurrent.ThreadPoolExecutor.Worker.runTask(Runnable)。这里在关闭线程池时就会导致中断所有线程。

除了InterruptedException 异常我们还发现了一个全新的异常java.util.concurrent.TimeoutException,此异常是用来描述任务执行时间超过了期望等待时间,也许是一直没有获取到锁,也许是还没有执行完成。

在innerGet代码片段中我们看到,如果线程在指定的时间无法获取到锁,那么就会得到一个超时异常。这个很好理解,比如如果执行一个非常耗时的网络任务,我们不希望任务一直等待从而占用大量的资源,可能在一定时间后就会希望取消此操作。此时超时异常很好的描述了这种需求。

与此同时,如果取消了一个任务,那么再次从任务中获取执行结果,那么将会得到一个任务被取消的异常java.util.concurrent.CancellationException。

除了上述异常外,还将得到一个java.util.concurrent.ExecutionException异常,

这是因为我们的提交的任务java.util.concurrent.Callable在call()方法中允许抛出任何异常,另外常规的线程执行也可能抛出一个RuntimeException,所以这里简单包装了下所有异常,当作执行过程中发生的异常ExecutionException抛出。

以上就是整个异常体系,所有并发操作的异常都可以归结于上述几类。

很多情况下处理时间长度都是用java.util.concurrent.TimeUnit,这是一个枚举类型,用来描述时间长度。其中内置了一些长度的单位。其中包括纳秒、微秒、毫秒、秒、分、时、天。例如超时操作5秒,可以使用

Future.get(5,TimeUnit.SECONDS) 或者 Future.get(5000L,TimeUnit.MILLISECONDS)

当然一种单位的时间转换成另一种单位的时间也是非常方便的。另外还有线程的sleep/join以及对象的wait操作的便捷操作。