Android Thread 101 (Part II) — Lifecycle of a Thread?

Jimmy Liu
Kuo’s Funhouse
Published in
14 min readMar 11, 2022

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] :

  1. NEW — a state when a thread has been created.
  2. RUNNABLE — a state when the Thread execute runnable, this is achieved by calling Thread#start().
  3. BLOCKED — a state when failed in competing with another thread for the ownership of the same monitor (explained later).
  4. 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().
  5. 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().
  6. TERMINATED — as the name stated, this is the state when the current thread is either completed or interrupted.
Life Cycle of a Thread in Java [ 1 ]

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
Brief walkthrough of how Thread is created by creating start( ) [ 3 ] [ 4 ]

Terminated

A thread can become terminated in the following situations :

  1. When the thread has completed its task
  2. If exception occur during the execution
  3. 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 :

monitor as Building [ 3 ]

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 :

  1. Entering the Building : invoking the syncMethod, thus the monitor region has been reached.
  2. Walking in the Hallway : the entry set is being proceeded.
  3. Check if the Special Room is Empty : The thread is trying to acquire the ownership of the monitor of obj.
  4. (If room is empty) Enters Special Room : The thread has acquired the ownership of the obj’s monitor, and proceed with the critical section
  5. (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.
  6. 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 :

[ 4 ]

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
}
}
Process flow of calling Thread.sleep [ 5 ] [ 6 ]

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 :

Process flow of Wait [ 5 ][ 6 ][ 7 ]

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:

Processing Flow when calling Park [ 10][ 11 ]

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

Processing Flow when calling Unpark [ 9 ][ 10 ]

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

--

--

Jimmy Liu
Kuo’s Funhouse

App Developer who enjoy learning from the ground up.