In the last post, we looked at the idea of threads and multi-threading in Java, and how to create code that could be run as a thread in the background.
In this post, we’ll look at the lifecycle of a thread. When we control threads manually, we have to be aware of the various states that a thread can be in, and how to transition between states.
Lifecycle of Threads
Threads can be in three states: blocked, dead and runnable/running. “Runnable” in this context does not refer to the Runnable
interface, but to the state of the thread itself.
The following diagram shows the different runtime states of a thread and what events cause transitions between states.
Starting a Thread
We can create a new Java thread object using the new
keyword. This only creates a Java object in memory; it does not create a low-level operating system thread. This is only done when the start()
method of the thread is called.
After the start()
method is called, the actual low-level thread is spawned and becomes runnable. A runnable thread is not necessarily running: a runnable thread only runs when the operating system provides the necessary resources.
A running thread will not necessarily keep running continuously. The operating system, through its scheduler, controls the allocation of processing time on whatever basis it uses: pre-emptive, time slicing or co-operative.
Every thread has a name so that it can be identified. We can use the same name for multiple threads if we’d like. If we don’t specify a name in the thread constructor, a new name will automatically be generated.
Blocked and Dead Threads
A thread can become blocked, i.e. it no longer receives resources and is therefore no longer runnable. It can become blocked for a number of reasons:
-
The thread is waiting on I/O that is blocked.
-
The
sleep()
method has been called, with a number of milliseconds specified for the sleep period. -
The
suspend()
method has been called. Thesuspend()
method is deprecated and should not be used because it is unsafe. It can lead to deadlock where the suspended thread holds an object lock that another thread needs to be able to resume the suspended thread. -
It is waiting for an object lock/monitor (we’ll look at this in a later post).
A thread becomes unblocked when:
-
The I/O for which it was waiting becomes free.
-
The time allocated to the
sleep()
method has elapsed. -
The
resume()
method has been called on a suspended thread. Theresume()
has been deprecated for the same reasons as thesuspend()
method. -
The
notify()
ornotifyAll()
method has advised the waiting thread(s) to retry the monitor.
A thread can die for two reasons:
-
It dies naturally when its
run()
method exits normally. -
It dies abruptly either when an uncaught exception terminates the
run()
method, or if thestop()
method is called.
When the stop()
method is called, the thread is forced to stop whatever it is doing abnormally, and throws a newly created ThreadDeath
error object. The stop()
method is deprecated and should not be used because it is unsafe: it can leave the data on which the thread was operating in an inconsistent state. Most uses of stop()
should be replaced by code that modifies some variable to indicate that the target thread must stop running.
Thread Priority
Threads are allocated the same priority as their parent thread, i.e. the main thread that created them. We can change this to a different priority, either before the thread starts running or during its execution by using the setPriority()
method.
The setPriority()
method takes an int
parameter with values ranging from 1 to 10. The Thread
class provides the constants Thread.MIN_PRIORITY
with the value of 1, Thread.NORM_PRIORITY
with the value of 5 and Thread.MAX_PRIORITY
with the value of 10. Keep in mind, however, that not all operating systems have 10 levels of priority – some have more, some have less.
Joining a Thread
After the main program has started any threads, it can continue its processing in parallel with these threads. If it needs to know that a particular thread has completed its work, it must call the join()
method on that thread.
At this point, the main program will go into a blocked state until the joined thread has died. When the thread has died, the main program will become unblocked and be able to proceed and exit if necessary. There are two join()
methods: one will wait indefinitely; the other has a timeout after which the main program will proceed regardless.
Yielding Control
A well-behaved thread executing a compute-intensive method should call the yield()
method occasionally. The yield()
method moves the running thread to the back of the runnable queue, and gives other equal priority threads a chance to run. We should call the yield()
method in case the operating system uses a cooperative approach, because all other threads may then starve by not getting enough machine cycles. This should not be required in the case of a pre-emptive or time-sliced operating system.
The yield()
method is not guaranteed to work, as final control depends on the thread scheduler of the specific operating system. A better solution would be to call the Thread.sleep()
method occasionally within a compute-intensive thread.
Conclusion
A bit more of the iceberg of threads has been exposed, although there’s still a lot more to come!
In the next few posts we’ll look at how to control threads, how to interrupt them and handle them being interrupted. We’ll also investigate the use of the synchronized
keyword, and the wait()
and notify()
methods of the Object
class. Then we’ll look at the exciting classes in the java.util.concurrent
package which make creating, running and controlling threads vastly easier.
Please share your comments.
Until then, stay safe and keep coding!