Sharing, blocking, and fairness
Just like the process life cycle, threads also have a life cycle. The following figure shows the various thread states. It shows three threads, t1, t2, and t3, in the Runnable, Running, and Timed Wait states. Here is a brief explanation of each state:
- New: When a Thread object is just created. The thread is not alive, as yet.
- Runnable: When the start() function is called on the thread object, its state is changed to runnable. As shown in the following diagram, a thread scheduler kicks in to decide when to schedule this thread to be run.
- Running: Eventually, the thread scheduler picks one of the threads from the runnable thread pool and changes its state to Running. This is when the the thread starts executing. The CPU starts the execution of this thread.
- Blocked: The thread is waiting for a monitor lock. As noted previously, for a shared resource such as a mutable memory data structure, only the thread can access/read/mutate it. While a thread has the lock, other threads will be blocked.
- Waiting: Wait for another thread to perform an action. Threads commonly block while doing I/O.
- Timed Wait: The thread waits for an event for a finite amount of time.
- Terminated: The thread is dead and cannot go back to any other state.
A thread goes back to the Runnable state once the event it waited for happens:
As shown in the preceding diagram, a blocking thread is expensive and wasteful. Why is this so? Remember, a thread is a resource itself. Instead of a thread that is just blocked and doing nothing, it would be far more optimal to employ it for processing something else. Wouldn't it be good to allocate the thread to do something useful?
Keeping the critical sections small is one way to be fair to all threads. No thread holds the lock for a long time (although this is can be altered).
Could we avoid blocking the thread and instead use it for something else? Well, that brings us to the theme of asynchronous versus synchronous executions.