The Ultimate Guide to Java Concurrency (Thread Creation and Lifecycle)

Shubhranshu Gupta
4 min readJul 5, 2024

In our previous blog, we explored the differences between processes and threads in Java, focusing on their memory structures and use cases. Now, let’s dive deeper into how we can create and manage threads in Java, and understand the various states a thread goes through during its lifecycle.

Creating Threads in Java

Java provides two main ways to create a thread:

  • Implementing the Runnable Interface:

The Runnable interface should be implemented by any class whose instances are intended to be executed by a thread. The class must define a method of no arguments called run.

public class CustomRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread is running...");
}
}

public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new CustomRunnable());
thread.start();
}
}
  • Extending the Thread Class:

The Thread class itself implements Runnable, so creating a subclass of Thread and overriding its run method is another way to create a thread.

public class CustomThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running...");
}
}

public class Main {
public static void main(String[] args) {
CustomThread thread = new CustomThread();
thread.start();
}
}

Why Two Ways to Create Threads in Java?

  • Separation of Concerns: By implementing Runnable, you keep the task to be performed separately from the thread management. This allows you to reuse the task code with different threading mechanisms (e.g., using thread pools).
  • Flexibility: A class can implement multiple interfaces, but it can only extend one class due to Java’s single inheritance model. By implementing Runnable, you avoid limiting your class's ability to extend another class.
  • Convenience: If your class already extends Thread, it inherits methods like start, sleep, and interrupt, which can be directly used within the class without needing an additional Thread object.

Thread Lifecycle in Java

A thread in Java goes through various states from its creation to its termination. The lifecycle states of a thread are:

Java Thread Lifecycle
  1. New: A thread is in the new state if you create an instance of the Thread class but have not yet started it using the start method.
  2. Runnable: After invoking the start method, the thread is in the runnable state. A thread in this state is ready to run and is awaiting CPU cycles.
  3. Blocked: A thread enters the blocked state when it is waiting to acquire a monitor lock to enter a synchronized block/method or while performing an I/O task.
  4. Waiting: A thread that is waiting indefinitely for another thread to perform a particular action is in this state. Methods like Object.wait() or Thread.join() can cause a thread to enter this state.
  5. Timed Waiting: A thread that is waiting for another thread to perform a particular action for a specified amount of time is in this state. Methods like Thread.sleep() or Object.wait(long timeout) cause a thread to enter this state.
  6. Terminated: A thread that has exited is in this state. A thread enters the terminated state when its run method completes or terminates by throwing an exception.

Example of Thread Lifecycle

Let’s see an example that illustrates some of these states:

public class LifecycleRunnable implements Runnable {
@Override
public void run() {
try {
Thread.sleep(2000);
syncronizedMethod();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public void syncronizedMethod() {
synchronized (this) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}


public class ThreadLifecycleExample {
public static void main(String[] args) throws InterruptedException {
LifecycleRunnable lifecycleRunnable = new LifecycleRunnable();

Thread thread1 = new Thread(lifecycleRunnable);
Thread thread2 = new Thread(lifecycleRunnable);
thread2.start();

System.out.println("Thread1 state: " + thread1.getState()); // NEW
thread1.start();
System.out.println("Thread1 state: " + thread1.getState()); // RUNNABLE

Thread.sleep(100); // Main thread sleeps for a bit
System.out.println("Thread1 state: " + thread1.getState()); // TIMED_WAITING

Thread.sleep(2000); // Main thread sleeps for 2sec
System.out.println("Thread1 state: " + thread1.getState()); // BLOCKED

thread1.join();
System.out.println("Thread1 state: " + thread1.getState()); // TERMINATED

}
}

Flow Summary

  • LifecycleRunnable instance created.
  • thread1 and thread2 are created but not started.
  • thread2 is started and enters the run() method, sleeping for 2000ms.
  • thread1 is checked and is NEW.
  • thread1 is started and enters the run() method, sleeping for 2000ms.
  • The main thread sleeps for 100ms, and thread1 is TIMED_WAITING.
  • The main thread sleeps for 2000ms, and thread1 is likely BLOCKED due to thread2 holding the lock.
  • The main thread waits for thread1 (due to the join method, which we will discuss in the next blog) to finish, resulting in the TERMINATED state.

This step-by-step breakdown should help you understand the flow and the state changes of the threads in the ThreadLifecycleExample class.

Conclusion

Creating threads in Java is straightforward, but understanding their lifecycle is crucial for developing robust multithreaded applications. By knowing the different states a thread can be in, and how to manage these states, you can write more efficient and effective Java programs.

Stay tuned for more insights into Java’s threading model and advanced concurrency concepts in upcoming blogs!

Reference

--

--