Task cancellation
An activity is cancellable if external code can move it to completion before its normal completion.
There is no safe way to preemptively stop a thread in Java, and therefore no safe way to preemptively stop a task. There are only cooperative mechanisms, by which the task and the code requesting cancellation follow an agree-upon protocol.
A task that wants to be cancellable must have a cancellation policy that specifies the “how”, “when”, and “what” of cancellation–how other code can request cancellation, when the task checks whether cancellation has been requested, and what actions the task takes in response to a cancellation request.
Interruption
Thread interrution is a cooperative mechanism for a thread to signal another thread that it should, at its convenience and if it feels like it, stop what it is doing and do something else.
Each thread has a boolean interrupted status; interrupting a thread sets its interrupted status to true. Thread
contains methods for interrupting a thread and querying the interrupted status of a thread:
1 | public class Thread { |
The interrupt
method interrupts the target thread, and isInterrupted
returns the interupted status of the target thread. The poorly named static interrupted
method clears the interrupted status of the current thread and returns its previous value; this is the only way to clear the interrupted status.
Blocking library methods like Thread.sleep
and Object.wait
try to detect when a thread has been interrupted and return early. They respond to interruption by clearing the interrupted status and throwing InterruptedException
, indicating that the blocking operation completed early due to interruption. The JVM makes no guarantees on how quickly a blocking method will detect interruption, but in practice this happens reasonably quickly.
If a thread is interrupted when it is not blocked, its interrupted status is set, and it is up to the activity being cancelled to poll the interrupted status to detect interruption. In this way interruption is “sticky”–if it doesn’t trigger an InterruptedException, evidence of interruption persists until someone deliberately clears the interrupted status.
Calling interrupt does not necessarily stop the target thread from doing what it is doing; it merely delivers the message that interruption has been requested.
A good way to think about interruption is that it does not actually interrupt a running thread; it just requests that the thread interrupt itself at the next convenient opportunity (cancellation points). Some methods, sunch as wait
, sleep
and join
take such request or encounter an already set interrupt status upon entry. Well behaved methods may totally ignore such requests so long as they leave the interruption request in place so that calling code can do something with it. Poorly behaved methods swallow the interrupt request, thus denying code further up the call stack the opportunity to act on it.
The static interrupted
method should be used with caution, because it cleans the current thread’s interrupted status. If you call interrupted
and it returns true
, unless you are planning to swallow the interruption, you should do something with it–either throw InterruptedException
or restore the interrupted status by calling interrupt
again.
Interruption policies
An interruption policy determines how a thread interprets an interruption request–what it does (if anything) when one is detected, what units of work are considered atomic with respect to interruption, and how quickly it reacts to interruption.
Tasks do not execute in threads they own; they borrow threads owned by a service such as a thread pool. Code that doesn’t own the thread (for a thread pool, any code outside of the thread pool implementation) should be careful to preserve the interruptted status so that the owning code can evetually act on it, even if the “guest” code acts on the interruption as well.
This is why most blocking library methods simply throw InterruptedException
in response to an interrupt. They will never execute in a thread they own, so they implement the most reasonable cancellation policyfor task or library code: get out of the way as quickly as possible and communicate the interruption back to the caller so that code higher up on the call stack can take further action.
A task needn’t necessarily drop everything when it detects an interruption request–it can choose to postpone it until a more opportune time by remembering that it was interrupted, finishing the task it was performing, and then throwing InterruptedException
or otherwise indicating interruption. This techinique can protect data structures from corruption when an acitivity is interrupted in the middle of an update.
A task should not assume anything about the interruption policy of its executing thread unless it is explicitly designed to run within a service that has specifc interrutpion policy. Whether a task interprets interruption as cancellation or takes some other action on interruption, it should take care to preserve the executing thread’s interrutpion status. If it is not simply going to propagate InterruptedException
to its caller, it should restore the interruption status after catching InterruptedException
:
Thread.currentThread().interrupt()
;
Just as task code should not make assumption about what interruption means to its executing thread, cancellation code should not make assumption about the interruption policy of arbitrary thread. A thread should be interrupted only by its owner; the owner can encapsulate knowledge of the thread’s interruption policy in an appropriate cancellation mechanism such as shutdown method.
Responding to interruption
When you call an interruptible blocking method such as Thread.sleep
or BlockingQueue.put
, there are two practical strategies for handling InterruptedException
:
- Propagate the exception (possibly after some task specific cleanup), making your method an interruptible blocking method, too; or
- Restore the interruption status so that code higher up on the call stack can deal with it.
If you don’t want to or cannot propagate InterruptedException
(perhaps because your task is defined by a Runnable
), you need to find another way to preserve the interruption request. The standard way to do this is to restore the interrupted status by calling interrupt
again. What you should not do is swallow the InterruptedException
by catching it and doing nothing in the catch
block, unless your code is actually implementting the interruption policy for a thread.
Activities that do not support cancellation but still call interruptible blocking methods will have to call them in a loop, retrying when interruption is detected. In this case, they should save the interruption status locally and restore it just before returning.
If your code does not call interruptible blokcing methods, it can still be made responsive to interruption by polling the current thread’s interrupted status throughout the task code. Choosing a polling frequency is a tradeoff between efficiency and responsiveness.
Cancellation can involve state other than the interruption status; interruption can be used to get the thread’s attention, and information stored elsewhere by the interrupting thread can be used to provide further instructions for the interrupted thread. For example, when a worker thread owned by a ThreadPoolExecutor
detects interruption, it checks whether the pool is being shut down. If so, it performs some pool cleanup before terminating; otherwise it may create a new thread to restore the thread pool to the desired size.
Cancellation via Future
ExecutorService.submit
returns a Future
describing the task. Future
has a cancel
method that takes a boolean argument, mayInterruptIfRunning
, and returns a value indicating whether the cancellation attempt was successful. (This tells you only whether it was able to deliver the interruption, not whether the task detected and acted on it.) When mayInterruptIfRunning
is true
and the task is currently running in some thread, then that thread is interrupted. Setting this argument to false
means “don’t run this task if it hasn’t starated yet”, and should be used for tasks that are not designed to handle interruption.
Since you shouldn’t interrupt a thread unless you know its interruption policy, when is it OK to call cancel
with an argument of true
? The task execution threads created by the standard Executor
implementations implement an interruption policy that lets tasks be cancelled using interruption, so it is safe to set mayInterruptIfRunning
when cancelling tasks through their Futures
when they are running in a standard Executor
. You should not interrupt a pool thread directly when attempting to cancel a task, because you won’t know what task is running when the interrupted request is delivered–do this only through the task’s Future
.
Non-interruptible blocking
Not all blocking methods or blocking mechanism are responsive to interruption; if a thread is blocked performing synchronous socket I/O or waiting to acquire an intrinsic lock, interruption has no effect other than setting the thread’s interrupted status. In this case, you can use technique for encapsulating nonstandard cancellatoin. For example, you can override the Thread.interrupt
method to explicitly close the socket to make any threads blocked in read
or write
throw a SocketException
.
Stopping a thread-based service
As with any other encapsulated object, thread ownership is not transitive: the application may own the service and the service may own the worker threads, but the application doesn’t own the worker threads and therefore should not attempt to stop them directly. Instead the service should provide lifecycle methods for shutting itself down that also shut down the owned threads.
ExecutorService
shutdown
In Chapter 6, we saw that ExecutorService
offers two way to shut down:
- Gracefully shutdown with
shutdown
, and abrupt shutdown withshutdownNow
. - In an abrupt shutdown,
shutdownNow
returns the list of tasks that had not yet started after attempting to cancel all actively executing tasks.
The two differernt termination options offer a tradeoff between safety and responsiveness.
More sophisticated programs are likely to encapsulate an ExecutorService
behind a higher-level service that provides its own lifecycle mehtod. Encapsulating an ExecutorService
extends the ownership chain from application to service to thread by adding another link; each member of the chain manages the lifecycle of the services or threads it owns.
Poison pills
Anoter way to convince a producer-consumer service to shut down is with a poison pill: a recognizable object placed on the queue that means “when you get this, stop.” With a FIFO queue, poison pills ensure that consumers finish the work on their queue before shutting down, since any work submitted prior to submitting the poison pill will be retrieved before the pill; producer should not submit any work after putting a poison pill on the queue.
Poison pills work only when the number of producers and consumers is known. For multiple producers, each producer place a pill on the queue and having the consumer stop only when it receives Nproducers pills. It can be extended to multiple consumers by having each prodcuer place Nconsumers pills on the queue. Poison pills work reliably only with unbounded queues.
Limitation of shutdownNow
When an ExecutorService
is shut down abruptly with shutdownNow
, it attempts to cancel the tasks currently in progress and returns a list of tasks that were submitted but never started so that they can be logged or saved for later processing. However, there is no general way to find out which tasks started but did not complete.
TrackingExecutor
below shows a techinque for determining which tasks were in progress at shutdown time. By encapsulating an ExecutorService
and instrumenting execute
(similar submit
) to remember which tasks were cancelled after shutdown, TrackingExecutor
can identify which tasks started but did not complete normally:
1 | public class TrackingExecutor extends AbstractExecutorService { |
TrackingExecutor
has an unavoidable race condition that could make it yield false positive: tasks that are identified as cancelled but actually completed. This arises because the thread pool could be shut down between when the last instruction of the task executes and when the pool records the task as complete. This is not a problem if task are idempotent.
Handling abnormal thread termination
The leading cause of premature thread death is RuntimeException
. Because these exceptions indicate a programming error or other unrecoverable problem, they are generally not caught. Instead they propagate all the way up the stack, at which point the default behaviors is to print a stack trace on the console and let the thread terminate.
Task-processing threads such as the worker threads in a thread pool or the Swing event dispatch thread spend their whole life calling unknown code through an abstraction barrier like Runnable
, and these threads should be very skeptical that the code they call will be well bahaved. Accordingly, these facilities should call tasks within a try-catch
block that catches unchecked exceptions, or within a try-finally
block to ensure that if the thread exits abnormally the framework is informed of this and can take corrective action.
For example, for a Runnable
you need to catch exception in the run
method:
1 | public class FailToCatch { |
Since Runnable
doesn’t throw exception, this try-catch
block won’t catch the exception, so the thread will be ternimated because of uncaught exception. Instead, you can catch in the run
method, the code below illustrate a way to structure a worker thread within a thread pool:
1 | public void run() { |
Uncaught exception handlers
The Thread API also provides the UncaughtExceptionHandler
facility, which lets you detect when a thread dies due to an uncaught exception.
When a thread exits due to an uncaught exception, the JVM reports this event to an application provided UncaughteExceptionHandler; if no handler exits, the default behavior is to print the stack trace to
System.err`.
1 | public class CaughtByHandler { |
When it comes to set an UncaughtExceptionHandler
for pool threads, you need to be cautious since you don’t which thread the task will be run on. For example, when catching exception in execute
, the below one doesn’t work:
1 | public class FailToCatchExecute { |
Because the Runnable
we set the UncaughtExceptionHandler
on is not the thread which will run the task.
Instead you can set the handler in the run
method, since we knows which thread is running the task at this point:
1 | public class CatchExecute { |
Also, you can provide a ThreadFactory
to the Executor
constructor, so it allows the owner of the thread to change its UncaughtExceptionHandler
. The standard thread pools allow an uncaught task exception to termnated the pool thread, but use a try-finally
block to be notified when this happens so the thread can be replaced.
Exceptions thrown from tasks make it to the uncaught exception handler only for tasks submitted with execute
; for tasks submitted with submit
, any thrown exception, checked or not, is considered to be part of the task’s return status. If a task submitted with submit
terminates with an exception, it is rethrown by Future.get
, wrapped in an ExecutionException
.
JVM shutdown
The JVM can shut down in either an orderly or abrupt manner. An orderly shutdown is initiated when the last normal (nondaemon) thread terminates, someone calls System.exit
. It can also be shut down abruptly by calling Runtime.halt
or by killing the JVP process through the operating system.
Shutdown hooks
In an orderly shutdown, the JVM first starts all registered shutdown hooks. The JVM makes no guarantees on the order in hwich shutdown hooks are started. When all shutdown hooks have completed, the JVM may choose to run finalizers if funFinalizersOnExit
is true
, and then halts. The JVM makes no attempts to stop or interrupt any application threads that are still running at shutdown time; they are abruptly terminated when the JVM eventually halts. If the shutdown hooks or finalizers don’t complete, then the orderly shutdown process “hangs” and the JVM must be shut abruptly. In an abrupt shutdown, the JVM is not required to do aything other than halt the JVM; shutdown hooks will not run.
Shutdown hooks should be thread-safe. Further, they should not assumptions about the state of the application or about why the JVM is shutting down. Finally, they should exit as quickly as possible.
Daemon threads
Sometimes you want to create a thread that performs some helper function but you don’t want the existence of this thread to prevent the JVM from shutting down. This is what daemon threads are for.
Threads are devided into two types: normal threads and daemon threads. When the JVM starts up, all the threads it creates (such as garbage collector and other housekeeping threads) are daemon threads, except the main thread. When a new thread is created, it inhertis the daemon status of the thread that creates it, so by default any threads created by the main thread are also normal threads.
Normal threads and daemon threads differs only in what happens when they exit. When a thread exits, the JVM performs an inventory of running threads, and if the only threads that are left are deamon threads, it initiates an orderly shutdown. When the JVM halts, any remaining daemon threads are abandoned–finally
blocks are not executed, stacks are not unwound–the JVM just exits.
Finalizer
The garbage collector treats objects that have a nontrivial finalizer
method specially: after they are reclaimed by the collector, finalizer
is called so that persistent resources can be released.
Since finalizers can run in a thread managed by the JVM, any state accessed by a finalizer will be accessed by more than one thread and therefore must be accessed with synchronization. Finalizer offers no guarantees on when or even if they run, and they impose a significant perfomance cost on objects with nontrivial finalizers.