Android Thread 101 (Part II) — Lifecycle of a Thread?
For anyone who don’t know what a Thread is, please feel free to take a look at Part I of Thread 101 series.
In Part II, we will be focusing on the Lifecycle of a Thread. Here are the topics that will be covered:
- Thread’s Lifecycle
- Non-Runnable
- Monitor
- Waiting/Timed Waiting — Sleep, Wait, Join, Park
- LockSupport
Thread’s Lifecycle
When an application is running, there will be tons of threads waiting to be proceeded. In order to determine which threads should execute and which ones are waiting, a thread’s state can be in one of the 6 states listed [1][2] :
- NEW — a state when a thread has been created.
- RUNNABLE — a state when the Thread execute runnable, this is achieved by calling Thread#start().
- BLOCKED — a state when failed in competing with another thread for the ownership of the same monitor (explained later).
- WAITING — a state when current thread is set to wait for the result from another thread. This can be achieved by calling wait(), join() or park().
- TIMED_WAITING — similar to WAITING, but will only wait for certain amount of time. This can be achieved by invoking sleep(time), wait(timeout), join(timeout), parkNanos(), parkUntil().
- TERMINATED — as the name stated, this is the state when the current thread is either completed or interrupted.
Now that you have the basic understanding of the different states, let’s take a closer look at each of them.
New
This is the state when a thread has been created
val thread = Thread() //thread.state == Thread.State.NEW
Runnable
Upon creating a Thread Object, we can start running the Thread by calling start(), bringing the Thread to Runnable state.
In this state, a native thread will be created as shown in Part I.
thread.start() //thread.state == Thread.State.RUNNABLE
Terminated
A thread can become terminated in the following situations :
- When the thread has completed its task
- If exception occur during the execution
- When the thread was interrupted, either by invoking Thread#interrupt or an exception occurred.
Now that the easy parts are done, we will focus on what happened when threads enter the Non-Runnable state.
Non-Runnable
A thread is said to be non-runnable when it is waiting or when it has been blocked, either way, monitor plays an important role in these states.
So what is a monitor ?
Synchronization
When working with applications, often there are more than one threads running. These threads might also be reading or writing the data that are shared among them, aka shared data.
Since reads and writes might occur at the same time, this will result in corrupted data value being loaded. This situation is what we called a race condition.
In order to avoid race condition, we use different kinds of synchronization tools, such as semaphore and mutex lock.
They both act like a key that allows a single thread accessing the shared data at any time.
Even though with the right implementation of these tools, we can solve the producer — consumer problem, but that is IF the programmers can implement it without any mistake.
So to release this burden from programmers, the term monitor was introduced.
Monitor
Monitor was invented by Per Brinch Hansen and C. A. R. Hoare, and were first implemented in Brinch Hansen’s Concurrent Pascal language [2].
It is a thread-safe class, object, or module that wraps around a mutex in order to safely allow access to a method or variable by more than one thread [2].
And in JVM, every object and class is logically associated with a single monitor [3].
Java’s monitor supports two kinds of thread synchronization: [4]
- Mutual Exclusion (Mutex) — this is implemented with object locks, allowing threads to use shared data without interfering with each other (ie Producer — Consumer situation).
- Cooperative — this is supported via wait and notify method of class object, enabling threads to work together towards a common goal (ie: Combining multiple data from network)
How does Monitor Work ?
To help you “see” what monitor does, you can think of monitor as a Building :
Within the building, there is a special room, in code we called it the critical section, that contains some data that only a single thread can enters at any time.
Here is how it looks like in code :
// <Building>
void syncMethod() {
// entry set synchronized(obj) {
// critical section <Special Room>
} // exit set
}
When a thread invokes syncMethod() the followings will occur :
- Entering the Building : invoking the syncMethod, thus the monitor region has been reached.
- Walking in the Hallway : the entry set is being proceeded.
- Check if the Special Room is Empty : The thread is trying to acquire the ownership of the monitor of obj.
- (If room is empty) Enters Special Room : The thread has acquired the ownership of the obj’s monitor, and proceed with the critical section
- (If room is occupied) Enters Wait Room : Depending on the reason why the thread did not acquire the ownership, it can be in the states of Blocked, Waiting or Time Waiting.
- Exiting the Special Room and the Building : Since an object can only have a single associated monitor, the thread will release the ownership of the monitor upon exiting, allowing other threads to compete in gaining the ownership.
Here is a good image that summarized what happened when synchronization occurred in both independent and cooperative situations :
With the understanding of what a monitor is, it’s time for us to go back to the lifecycle of a thread.
Blocked
As mentioned above, Blocked occurs when a thread is waiting for the ownership of the monitor to be released.
This can be achieved as follow :
Object obj = new Object();Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj) {/*do nothing*/}
}
});Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj) {
// Infinite Loop
for(;;) {
System.out.println("t1 STATE - " + t1.getState());
}
}
}
});t2.start();
t1.start();// RESULT
t1 STATE - BLOCKED
t1 STATE - BLOCKED
t1 STATE - BLOCKED
In this code, t1 and t2 both hold a runnable that performs synchronization based on the Object obj. Meaning that upon entering the critical section of either run functions, the monitor of obj will be acquired by one of them.
By invoking t2.start(), t2 has now acquired the ownership of the obj monitor. So when t1.start() is invoked, t1 will have to wait for t2 to release the ownership, which brings it to the Blocked state.
Waiting/ Timed Waiting
Waiting and Timed Waiting will be discussed in the same section because they shared many common methods, including sleep, wait, join, park and notify/notifyAll.
It is obvious that these functions causes thread to suspend, but how they do it are quite difference.
Sleep
sleep is a static function in the Thread class
By calling Thread.sleep(time), the current thread will be suspended and the ownership of the monitor will be held , as illustrated below :
thread {
synchronized(t) { // <- the thread will try to
acquire ownership of
the monitor of
object t
Thread.sleep(2000)
}
}// or
// Thread.sleep does not required synchronized block to run.
// because it runs its own synchronized block show below
public static void main(String[] args) {
try {Thread.sleep(2000}
catch(InterruptException e) {}
}
this will trigger the native code Thread_sleep in java_lang_Thread.cc, which blocks the current Thread.
public static void sleep(long millis, int nanos)
throws InterruptedException { Object lock = currentThread().lock;
synchronized (lock) {
// for a period of time (max = Long.MAX_VALUE).
for(long elapsed = 0L; elapsed < durationNanos;
elapsed = System.nanoTime() - startNanos) {
sleep(lock, millis, nanos); // <- this is native code
}}
Result
At the end, sleep will block the current thread for a period of time while holding the ownership of the monitor of object t.
Thus, stopping threads that synchronized the same object to run at all.
Example : Synchronizing on Same Objects
output :
The output shows that by calling Thread.sleep, other thread cannot get hold of the ownership of obj, thus proving that Thread.sleep does not release the ownership of object obj.
t1 Acquiring Ownership
t1 Acquired Ownership
t2 Acquiring Ownership
t1 Invoke Sleep
t1 Sleep after 2 seconds
t1 Releasing Ownership
t1 Released Ownership
t2 Acquires Ownership
t2 Releasing Ownership
t2 Released Ownership
Example : Synchronizing on Different Objects
Output :
t2 Acquiring Ownership
t2 Acquired Ownership
t2 Invoke Sleep
t1 Acquiring Ownership
t1 Acquires Ownership
t1 Invoke Sleep
t1 Sleep after 5 seconds
t1 Releasing Ownership
t1 Released Ownership
t2 Sleep after 5 seconds
t2 Releasing Ownership
t2 Released Ownership
Wait / Notify / NotifyAll
wait is a function in Object class
By calling wait, we will trigger the native function of Object_waitJI :
Unlike sleep that holds on to the ownership of the monitor, wait will release the ownership of the monitor.
Let’s use the examples above, but replace sleep by wait and see what happen.
Example : Wait on the same Object
Output
t1 Acquiring Ownership
t1 Acquires Ownership
t1 Invoke Sleep
t2 Acquiring Ownership
t2 Acquires Ownership
t2 Releasing Ownership
t2 Released Ownership
t1 Sleep after 5 seconds
t1 Releasing Ownership
t1 Released Ownership
From this result, we are sure that using wait will not hold the ownership, instead, it will release the ownership allowing other threads to do their business.
In order to wake up the waiting thread, we can use notify or notifyAll :
// Synchronized on the same object
synchronized (obj) {
System.out.println("t2 Acquires Ownership");
obj.notify();
System.out.println("t2 Releasing Ownership");
}
notify/notifyAll will only wake up the threads that are waiting for the same object’s monitor.
Join
join is a synchronized function in Thread class, which basically is a block that is synchronized to this.
By calling join, the following code will be triggered :
Note : final synchronized void join is equivalent as follow :
void fun join() {
synchronized(this) {}
}
As shown in the code, join is proceeded by triggering the wait function, which as stated before, it will release the ownership.
However, since this is a synchronized method, this means that it will only affect the monitor of the thread itself, while blocking other threads.
At the end, join acts similar to sleep, which will block the thread, but it can be woken either until the time is up or when it has completed its task.
Example : Join using the same/different Object
Using exactly the same code as wait example, but only add t1.join() between the two start function :
t1.start();
try {
t1.join();
}catch (InterruptedException e) {
// do nothing
}
t2.start();
In the previous example, the order to execute t1 and t2 can be arbitrary, since they are competing for the monitor of the same object.
However, with the help of join, t1 will block the thread, forcing t2 to wait while t1 completes its task, despite t2 might try to acquire the monitor of different object.
Result
t1 Acquiring Ownership
t1 Acquires Ownership
t1 Invoke Sleep
t1 Sleep after 2 seconds
t1 Releasing Ownership
t1 Released Ownership
t2 Acquiring Ownership
t2 Acquires Ownership
t2 Releasing Ownership
t2 Released Ownership
Example : Use join for Cooperative job
Let’s make a simple example, here we assumed all threads (t_init, t_multiply, t_sum) are fetching sum from the network and the answer to sum should be 2010:
sum = 0// The purpose of this
fun sumShouldBe2010() {
sum = 0
val t_init = Thread {
Log.i("THREAD_1 (START)", "sum = $sum")
Thread.sleep(1000)
sum = 100
Log.i("THREAD_1 (END)", "sum = $sum")
}
val t_multiply = Thread {
Log.i("THREAD_2 (START)", "sum = $sum")
Thread.sleep(1000)
sum *= 20
Log.i("THREAD_2 (END)", "sum = $sum")
}
val t_sum = Thread {
Log.i("THREAD_3 (START)", "sum = $sum")
Thread.sleep(1000)
sum+=10
Log.i("THREAD_3 (END)", "sum = $sum")
}
t_init.start()
t_multiply.start()
t_sum.start()
}
When we run the code above, you will find that the order of the threads is not consistent :
// Success
THREAD_1 (START): sum = 0
THREAD_2 (START): sum = 0
THREAD_3 (START): sum = 0
THREAD_1 (END): sum = 100
THREAD_2 (END): sum = 2000
THREAD_3 (END): sum = 2010// Failed
THREAD_2 (START): sum = 0
THREAD_3 (START): sum = 0
THREAD_1 (START): sum = 0
THREAD_2 (END): sum = 0
THREAD_3 (END): sum = 10
THREAD_1 (END): sum = 100
So what can we do ?
We can simply use join() method, remember to call it right after start() otherwise the next thread will start along with the current thread :
Log.d("THREAD_init", "Invoke Start")
t_init.start()
t_init.join()
Log.d("THREAD_multiply", "Invoke Start")
t_multiply.start()
t_multiply.join()
Log.d("THREAD_sum", "Invoke Start")
t_sum.start()
t_sum.join()// Consistent Result
THREAD_INIT : Invoke Start
THREAD_INIT (START): sum = 0
THREAD_INIT (END): sum = 100
THREAD_MULTIPLY : Invoke Start
THREAD_MULTIPLY (START): sum = 0
THREAD_MULTIPLY (END): sum = 2000
THREAD_SUM : Invoke Start
THREAD_SUM (START): sum = 0
THREAD_SUM (END): sum = 2010
By calling join(), we can get a consistent result and consistent thread execution order.
Park / Unpark
park/unpark are static method in LockSupport class. But since it involved the usage of Unsafe class, they are rarely used for application development.
LockSupport is a special class that contains only static functions that act as basic thread blocking primitives for creating locks and other synchronization classes.
If we take a closer look at these functions :
public static void unpark(Thread thread)public static void park()
public static void park(Object blocker)
public static void parkNanos(long nanos)
public static void parkNanos(Object blocker, long nanos)
public static void parkUntil(long deadline)
public static void parkUntil(Object blocker, long deadline)
We will find that they share 3 common functions :
- U.unpark(Thread) : called by unpark(Thread) only
- U.putObject(Thread, key, Object) : called by functions with Object parameter (ie parkUntil(Object blocker, long deadline)).
- U.park() : called by other functions except unpark(Thread)
Here U is an Unsafe class object from jdk.internal.misc package.
Unsafe class provides a way to do certain things that are otherwise impossible and that break well-established rules of the platform [ 8 ], such as :
- Directly access CPU and other hardware features
- Create an object but not run its constructor
- Create a truly anonymous class without the usual verification
- Manually manage off-heap memory
- more
Since Unsafe has such potential and danger, therefore, we seldom use it.
Now, let’s see what role do these three functions play.
U.putReference(Thread, key, Object)
This function is used to store the thread and argument. In this case, the argument is a blocker object.
This object (blocker) is recorded while the thread is blocked to permit monitoring and diagnostic tools to identify the reasons that threads are blocked [ 9 ]
U.park / U.unpark
As for park and unpark, we need to discuss them together since they come in pair.
park is used to disable the thread and unpark is used to resume the thread. This is done by changing the park state of the thread.
In order to understand how they work, we need to take a look at the native code.
Park
By calling U.park(), the Park(isAbsolute, time) function of the current thread in thread.cc will be triggered:
and the tls32_.park_state_ or park state of the current thread will be altered in the following ways [10]:
- If park state was kNoPermit (= 1), then the thread will be blocked and the lock will be released. The park state will also be set to kNoPermitWaiterWaiting (= 2). At the very end, park state will again be set to kNoPermit (= 1).
- If the park state is kPermitAvailable (= 0) at the beginning, then nothing will be done.
A brief introduction to tls32_
TLS stands for Thread Local Storage, which is a struct that stores info on the associated thread [11] and park_state_ is one of them. The number 32 stands for how many bits it uses.
park_state_ is an AtomicInteger that represents 3 status and will be kept within these values:
- kPermitAvailable when park_stat_ = 0
- kNoPermit when park_stat_= 1
- kNoPermitWaiterWaiting when park_stat_= 2
Unpark
Similar to park, unpark will trigger the following
By calling U.unpark(), the followings will occur :
- the current thread’s park state will be set to kPermitAvailable
- However, if the park state is kNoPermitWaiterWaiting, then futex will be used to wake up the thread, then set it to kPermitAvailable.
By understanding the mechanism behind park and unpark, we can see that the order of park and unpark plays an important role in what will happen to the thread.
Example : Park before Unpark
As expected, it prints :
Trigger Park
Unpark Triggered // after 10 seconds
Unpark
How about when Unpark occur before Park ?
Example : Unpark before Park
You can try to make a guess what will happen here.
Since the mechanism behind park and unpark is changing the park state.
So if unpark has set park state to kPermitAvailable (= 0), then when the code reaches park nothing will be done, meaning park will not be able to disable the thread.
As a result :
Unpark Triggered
// Pause 10 seconds
Trigger Park
UnPark // this will be called immediately
From the examples above, we know that :
The park function will block the current thread and holds the ownership of the monitor until permitted by unpark either before or after park is called.
Here are are summary for these functions’ properties :
Sleep
- does not release the ownership of monitor.
- cannot resume manually, except when interrupt occur
Wait
- release the ownership of monitor
- can be resumed manually by calling notify or notifyAll
Join
- does not release the ownership of monitor
- cannot resume manually, except when interrupt occur
- resumed once the thread has done its task by triggering notifyAll, allowing other threads to compete for the ownership of monitor
Park
- does not release the ownership of monitor
- can be resumed manually by calling Unpark
- If Unpark is called before Park, then the thread will not be blocked at all
Now that we have seen the lifecycle of a thread, next we should talk about how threads communicate with each other.
Reference
[ 1 ] Life Cycle of a Thread in Java
[ 2 ] Monitor, wiki
[ 3 ] Monitors — The Basic Idea of Java Synchronization
[ 4 ] Chapter 20 of Inside the Java Virtual Machine: Thread Synchronization
[ 5 ] Difference Between Wait And Park Methods In Java Thread
[ 5 ] java_lang_Thread.cc
[ 6 ] monitor.h
[ 7 ] object-inl.h
[ 8 ] The Unsafe Class: Unsafe at Any Speed, Ben Evans, May 5, 2020
[ 9 ] Class LockSupport
[ 10] sun_misc_Unsafe.cc
[ 11 ] thread.cc