Demystifying the Java Threads: Lesson 2 — Threads Lifecycle

Farrukh Masroor
7 min readMar 7, 2024

--

In this second installment of our series, “Demystifying the Java Threads,” we will delve deeper into the lifecycle of threads. Understanding the lifecycle of threads is essential for writing efficient and error-free multithreaded applications. Join us as we explore the various states and transitions that threads go through in their lifecycle, shedding light on this fundamental aspect of Java programming.

What we will learn in this lesson:

The life cycle of the thread

Synchronization and monitor locks

The life cycle of the thread

In the thread lifecycle, the threads transition from 6 different states they are defined by the enum State, the State enum is defined in the Thread class.

The first stage of a Thread in its journey is

NEW- Thread state for a thread that has not yet started,

Whenever we create an instance of a thread class the state of the thread is the New state.

1 package com.threads;

2 public class ThreadTest extends Thread{

3 @Override
4 public void run(){
5 System.out.println("The state of the thread"+Thread.currentThread().getName()+" is : "+Thread.currentThread().getState());
6 }

7 public static void main(String args[]){
8 ThreadTest obj= new ThreadTest();
9 obj.setName("New Thread 1");
10 System.out.println("The state of the thread is : "+obj.getState());
11 obj.start();
12 }
13 }

So when the above code executes line 10 the output will be -

The state of the thread is : NEW

RUNNABLE: A thread in the runnable state is executing in the Java virtual machine but it may be waiting for other resources from the operating system such as a processor, i.e. either running or ready for execution but it’s waiting for resource allocation.

When we call the start method on a thread, it transitions into the Runnable state. However, it's important to note that being in the RUNNABLE state does not necessarily mean that the thread is actively executing the code defined in its run method. This is because, in the RUNNABLE state, it's the responsibility of the thread scheduler to select and start the execution of a thread from among multiple threads that are running concurrently. The thread scheduler schedules threads based on their priority and certain algorithms, which we will discuss separately in our lesson on the Thread Scheduler.

Therefore, when the program reaches line 11, the state of our thread, “New Thread 1,” will transition to RUNNABLE, and then line 5 will be executed, printing the expected output.

The state of the thread New Thread 1 is : RUNNABLE

BLOCKED: Thread state for a thread blocked waiting for a monitor lock. A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method or reenter a synchronized block/method after calling. Don’t get overwhelmed by the terms used we will demystify all as we continue.

So what are synchronization and monitor locks?

In simpler terms, when multiple tasks are running simultaneously (as in a multi-threading environment) and they need to access the same resources, it’s important to ensure that only one task can use those resources at a time. Failure to do so could result in errors or incorrect outcomes.
Consider a scenario where you want to send money to a friend using your favorite UPI app. Suppose that the moment you’re about to hit the pay button, your account balance is 3500. You need to send 1500 to your friend. However, at the same time, your father sends you 3000. Both transactions access your bank account and read the same balance of 3500. One transaction debits 1500, making your balance 2000, while the other updates the balance to 6500. This creates a discrepancy and a potential loss for the bank. To handle such situations, we use the concept of monitor locks and synchronization to ensure that only one transaction can access the shared resource at a time.

When synchronization is used, each transaction is treated as a separate entity. Let’s assume Transaction A (Txn A) is the payment to your friend, and Transaction B (Txn B) is the money from your father. We design our code so that only one transaction can access your account at a time. For example, if Txn A locks your account, no other transaction can access it until Txn A is completed. After Txn A is completed successfully, your balance will be 2000. Then, when Txn B accesses your account, it will get the correct and updated balance of 5000 after completing its process. Synchronization helps manage this sharing by using a special object called a monitor, which acts like a lock. This ensures that only one task can hold the lock at any given time, preventing conflicts and errors.

So with this let’s continue to our lifecycle methods of Threads, and the next phase is

WAITING: A thread in the waiting state is waiting for another thread to perform a particular action. For example, a thread that has called Object.wait() on an object is waiting for another thread to call Object.notify() or Object.notifyAll() on that object. A thread that has called Thread.join() is waiting for a specified thread to terminate.

Okay, so these methods might seem overwhelming at first, but we’ll cover them in detail in our lesson on Inter Thread Communication.

Let’s consider our previous example. Suppose while Txn A had the lock to our bank account and we were in the process of sending money to our friend, we discovered that the bank server of our friend was down. One way to handle this situation is to cancel Txn A and roll back all the changes made by Txn A. However, this means we would have to start Txn A again from the beginning. Another approach is to let Txn A wait for some time and see if the server comes back up. We can do this using the wait method, that tells Txn A to wait while we give access to our account to Txn B. Now, Txn B has access to our synchronized code or account. Once Txn B completes its work, it will call notify to inform Txn A that it has finished its task and that Txn A can now continue. This way, we don't need to manually trigger Txn A again.

So now let’s move to the next lifecycle method

TIMED_WAITING: Thread state for a waiting thread with a specified waiting time. A thread is in the timed waiting state due to calling one of the following methods with a specified positive waiting time:

Thread.sleep()

Object.wait() with timeout

Thread.join() with timeout

LockSupport.parkNanos()

LockSupport.parkUntill()

TIMED_WAITING is pretty much the same as WAITING but here we specify a time limit while in WAITING there is no time limit for waiting.

The last and final state is

TERMINATED: This is the state of a dead thread. It’s in the TERMINATED state when it has either finished execution or was terminated abnormally.

Now let us understand the all-life cycle method using the code

package com.threads;

import java.util.Set;

public class ThreadTest extends Thread{

@Override
public void run(){
try {
System.out.println("Before starting the syncMethod "+Thread.currentThread().getName() + " : "+Thread.currentThread().getState());
syncMethod();
System.out.println("After ending the syncMethod "+Thread.currentThread().getName() + " : "+Thread.currentThread().getState());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

public static void main(String args[]) throws InterruptedException {
ThreadTest obj= new ThreadTest();
Thread thread1= new Thread(obj);
Thread thread2= new Thread(obj);
thread1.setName("my-thread-1");
thread2.setName("my-thread-2");
System.out.println("The state of the thread "+thread1.getName()+" is : "+thread1.getState());
System.out.println("The state of the thread "+thread2.getName()+" is : "+thread2.getState());
thread1.start();
thread2.start();
Thread.sleep(1000);
printThreadState();
Runnable runnable= ()->{System.out.println("another thread");};

Thread thread3= new Thread(runnable);
thread3.setName("my-thread-3");
thread3.start();
Thread.sleep(20000);
System.out.println("the state of thread "+thread3.getName()+ " is "+thread3.getState());
}

public synchronized void syncMethod() throws InterruptedException {
System.out.println("Starting the synchronize code for thread "+Thread.currentThread().getName());
Thread.sleep(4000);
}

public static void printThreadState(){
Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
threadSet.stream().filter(t->t.getName().contains("my-thread")).forEach(t->{
System.out.println("Current state of thread "+t.getName()+" state is "+t.getState());
});
}
}

the output of the above code will be

The state of the thread my-thread-1 is : NEW
The state of the thread my-thread-2 is : NEW
Before starting the syncMethod my-thread-1 : RUNNABLE
Before starting the syncMethod my-thread-2 : RUNNABLE
Starting the synchronize code for thread my-thread-1
Current state of thread my-thread-2 state is BLOCKED
Current state of thread my-thread-1 state is TIMED_WAITING
another thread
Starting the synchronize code for thread my-thread-2
After ending the syncMethod my-thread-1 : RUNNABLE
After ending the syncMethod my-thread-2 : RUNNABLE
the state of thread my-thread-3 is TERMINATED

Note the output will be a bit different every time you run this code as we have not synchronized the threads, which we will cover in our next lesson,

So let’s understand the life cycle with the code, in the above code snippet, we have created 3 threads namely my-thread-1, my-thread-2, and my-thread-3.

On lines 20 and 21 we have printed the state of my-thread-1, my-thread-2 and since we have not called the start method as of now the state will be NEW, after lines 22 and 23 both threads come in the RUNNABLE state, at line 8 we have called a synchronized method and since only a single thread can access the monitor locked synchronized block so the other thread will go in the BLOCKED state as printed in the output my-thread-1 got the access to synchronized block first so it stared executing the syncMethod and in the main thread we have called a method called printThreadState which will print the state of all the thread and it shows that the my-thread-2 state is BLOCKED.

Also, we have called the sleep method inside the syncMethod so we are also getting the state of my-thread-1 as TIMED_WAITING while it is in sleep, when the my-thread-1 completes its execution it will be TERMINATED and my-thread-2 will get access to the syncMethod.

from line 26 onwards we created another thread my-thread-3 and after the start of my-thread-3 we sleep the main thread so that our my-thread-3 will finish its execution and then we printed the state which shows that my-thread-3 has been TERMINATED.

So far we have completed two lessons on our journey to understand the Threads and in the next lesson, we will demystify how the threads communicate with each other.

--

--

Farrukh Masroor

Welcome to my corner of the tech world! I'm Farrukh Masroor, a seasoned software engineer with a passion for all things technology.