Today, we start with a question: can following code compiles?
// pre-condition: Thread#sleep throws InterruptedException
ExecutorService service = Executors.newCachedThreadPool();
service.submit(() -> {
Thread.sleep(1);
return null;
});
service.submit(() -> {
Thread.sleep(1);
});
The short answer is no and the compiler will complain the second invocation of sleep but not the first one:
service.submit(() -> {
Thread.sleep(1); // <------ here
});
Submit A Task
So what happens here? Let’s see the prototype of submit:
<T> Future<T> submit(Callable<T> task);
Very normal and meet our imagination. But in the second submit, do we really give a Callable
? No, we don’t return in the second submission. It’s actually a Runnable
.
Future<?> submit(Runnable task);
Comparing the prototype of run
and call
give us the detailed answer :
public interface Runnable {
public abstract void run();
}
public interface Callable<V> {
V call() throws Exception;
}
In the first submission, we give executor a callable, which return a null, so we can choose to not handle the interrupt exception. In the second one, we give executor a runnable, in which we have to handle the exception in run()
.
service.submit(new Callable<Void>() {
@Override
public Void call() throws Exception {
Thread.sleep(1); // <--- fine, for call throw exception
return null;
}
});
service.submit(new Runnable() {
@Override
public void run() {
Thread.sleep(1); // compiler error, unhandled exception
}
});
We enjoy the convenience of lambda, so we have to bear the misleading semantic it may bring.
Exception Handle in Executor
Through the above question, we get to know that we have to handle exception directly in run()
. But through which way we can handle the exception threw in call
? By some searching, we can find out the answer:
When we submit a Callable
job to executor, it will return a Future
to us. When we try to get the result of job by calling Future.get()
, it will throw ExecutionException
if provided callable threw exception in the past (the exception is stored in the Future). And the detailed exception can be inspected using the Throwable.getCause()
method.
Comparison
Now, we get to know there exists two different ways to handle exception when using executor to run our jobs.
- The first is to handle it right in the
run
; - The second is to handle it when getting the result from
Future
As far as I am concerned, using the callable give us more choice to decide when to catch our exception and when to throw it to upper code, which is more flexible and powerful to use. Maybe this is the reason why it is introduced with executor frameworks.
One more thing to notice is using Runnable#run
may cause some strange symptom that some task was the disappeared when thread dump. This is because some unchecked exception is thrown and executor just silent swallow it and will make programmer very confused. Even though we can register UncaughtExceptionHandler
, we may met the tricky differences between submit
and execute
, in which UncaughtExceptionHandler
will not be called as this SO question described.
The famous saying said: “Throw early, catch late”, but it is not so easy to decide when to catch it, isn’t it?
More
Written with StackEdit.
评论
发表评论