Understanding the Lifecycle and States of a Thread in Java

In Java, multithreading is a powerful feature that allows concurrent execution of two or more threads for maximum utilization of CPU. Understanding the lifecycle and states of a thread is crucial for writing efficient multithreaded applications. In this blog, we’ll explore the various states a thread can be in and the transitions between these states.

The Basics of Java Threads

🔷 What is a Thread?

In the context of programming, a thread is the smallest unit of execution within a process. A thread is a lightweight subprocess that can be managed independently by a scheduler. Each thread in a program operates within the context of a process, sharing the process’s resources such as memory and file handles, but it has its own execution stack and program counter.

🔷 Importance of Threads in Java

Concurrency: Allows multiple tasks to be in progress within a single program. This is essential for performing tasks like handling user inputs, processing background tasks, and updating the user interface simultaneously.

Parallelism: In a multi-core processor environment, threads can run in parallel, thus making better use of CPU resources and improving the overall performance of the application.

Responsiveness: Threads can enhance the responsiveness of an application. For instance, in a graphical user interface (GUI) application, a dedicated thread can be used to handle user inputs while other threads handle background tasks. This ensures that the application remains responsive to user actions.

Resource Sharing: Threads within the same process share memory and resources, which allows them to communicate and share information more efficiently than separate processes. This sharing reduces the overhead of memory usage and improves performance.

Efficient Use of Resources: Threads can be used to perform multiple tasks simultaneously within the same program, thereby making efficient use of system resources. For example, in a web server, multiple threads can handle multiple client requests concurrently.

🔷 Creating Threads: Briefly Explain the Two Ways to Create a Thread in        Java

Extending the Thread class

public class MyThread extends Thread {
    @Override
    public void run() {
        // Code to be executed by the thread
        for (int i = 1; i <= 5; i++) {
            System.out.println("Thread is running: " + i);
            try {
                Thread.sleep(500); // Simulate some work with sleep
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Thread execution completed");
  }

  public static void main(String[] args) {
     MyThread thread = new MyThread();
     thread.start(); // Start the thread
  }
}

Output:

Thread is running: 1
Thread is running: 2
Thread is running: 3
Thread is running: 4
Thread is running: 5
Thread execution completed

Explanation:

  1. Subclass of Thread: We create a class MyThread that extends Thread.
  2. Override run() Method: The run() method is overridden to define the task that the thread will perform. In this example, the task is to print numbers from 1 to 5, simulating some work with Thread.sleep(500).
  3. Instantiate and Start the Thread: In the main method, we create an instance of MyThread and start it by calling start(). The start() method invokes the run() method in a new thread of execution.

Key Points:

  • Separation of Tasks: Extending Thread makes it easy to define a separate task within a new class.
  • Thread Initialization: The start() method is crucial. It initiates the new thread and calls the run() method. Simply calling run() directly will not start a new thread; it will execute in the current thread.
  • Thread Lifecycle: Once started, the thread transitions through its lifecycle states (e.g., NEW to RUNNABLE to TERMINATED) as it executes the run() method.

Implementing the Runnable Interface Runnable Interface

public class MyRunnable implements Runnable {
    @Override
    public void run() {
       // Code to be executed by the thread
       for (int i = 1; i <= 5; i++) {
           System.out.println("Thread is running: " + i);
           try {
               Thread.sleep(500); // Simulate some work with sleep
           } catch (InterruptedException e) {
              e.printStackTrace();
           }
       }
       System.out.println("Thread execution completed");
  }

public static void main(String[] args) {
   MyRunnable myRunnable = new MyRunnable();
   Thread thread = new Thread(myRunnable);
   thread.start(); // Start the thread
 }
}

Output:

Thread is running: 1
Thread is running: 2
Thread is running: 3
Thread is running: 4
Thread is running: 5
Thread execution completed

Explanation:

  • Implement Runnable: We create a class MyRunnable that implements the Runnable interface.
  • Override run() Method: The run() method is overridden to define the task that the thread will perform. In this example, the task is to print numbers from 1 to 5, simulating some work with Thread.sleep(500).
  • Instantiate Thread and Start It: In the main method, we create an instance of MyRunnable and pass it to a new Thread object. We then start the thread by calling start(). The start() method invokes the run() method in a new thread of execution.

Key Points:

  • Separation of Task and Thread: By implementing Runnable, you separate the task definition from the thread creation and management, promoting cleaner and more modular code.
  • Flexibility: Since Java supports single inheritance, using Runnable is more flexible as it allows your class to extend another class if needed.
  • Thread Initialization: The start() method on the Thread object is essential. It initiates the new thread and calls the run() method on the Runnable instance. Simply calling run() directly will not start a new thread; it will execute in the current thread.

Thread Lifecycle Overview

In Java, threads go through several distinct states from their creation to their termination. Understanding these states is crucial for effectively managing multithreaded applications. Below, we introduce the phases of a thread’s lifecycle and provide an illustrative diagram for better comprehension.

Thread Lifecycle Overview

  • New: A thread that has been created but not yet started.
Thread thread = new Thread(new MyRunnable());

Runnable: A thread that is ready to run and waiting for CPU cycles.

thread.start();

Blocked: A thread that is blocked waiting for a monitor lock. It occurs while Entering a synchronized block or method when another thread holds the lock.

synchronized (lock) {
// thread code
}

Waiting: A thread that is indefinitely paused, awaiting a specific action from another thread. You can enter this state by calling wait() on an object.

synchronized (lock) {
lock.wait();
}

Timed Waiting: A thread that is waiting for another thread to perform an action within a time limit. You can enter this state by Calling sleep(), wait(long timeout), join(long timeout), LockSupport.parkNanos(), or LockSupport.parkUntil().

Thread.sleep(1000);

Terminated: A thread that has completed its execution.It occurs When the run() method finishes execution.

public void run() {
// thread execution code
System.out.println("Thread is running");
}

Optimize Your App's Performance with Expert Thread Management. Hire Our Developers Now!

Transition Between States

Example 1:

public class ThreadLifecycleExample {
   public static void main(String[] args) {
       Thread thread = new Thread(new MyRunnable());
       System.out.println(thread.getState()); // NEW
       thread.start();
       System.out.println(thread.getState());
// RUNNABLE
       try {
           thread.join();
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       System.out.println(thread.getState());
// TERMINATED
   }
}

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

Output:

NEW
RUNNABLE
Thread is running
TERMINATED

Example 2: Synchronization and State Transitions

public class ThreadSyncExample {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(new SyncTask());
        Thread thread2 = new Thread(new SyncTask());

        thread1.start();
        thread2.start();
    }

    static class SyncTask implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " has acquired the lock");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
System.out.println(Thread.currentThread().getName() + " is releasing the lock");
          }
       }
   }
}

Output:

Thread-0 has acquired the lock
Thread-0 is releasing the lock
Thread-1 has acquired the lock
Thread-1 is releasing the lock

In this example, we will create a class Counter with a method increment() that increments a shared counter. We will use a synchronized block to ensure that the counter is incremented safely by multiple threads.

Example: Synchronization with a Shared Counter

public class SynchronizedExample {
    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread thread1 = new Thread(new CounterTask(counter));
        Thread thread2 = new Thread(new CounterTask(counter));

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }

        System.out.println("Final counter value: " + counter.getCount());
    }
}

class Counter {
     private int count = 0;
     private final Object lock = new Object();

     public void increment() {
         synchronized (lock) {
               // thread code
                  count++;
System.out.println(Thread.currentThread().getName() + " incremented count to: " + count);
         }
    }
  
    public int getCount() {
        return count;
    }
}

class CounterTask implements Runnable {
    private final Counter counter;

    public CounterTask(Counter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
             counter.increment();
             try {
                 Thread.sleep(100); // To simulate some work being done
             } catch (InterruptedException e) {
               e.printStackTrace();
             }
         }
     }
}

Output:

Thread-0 incremented count to: 1
Thread-1 incremented count to: 2
Thread-0 incremented count to: 3
Thread-1 incremented count to: 4
Thread-0 incremented count to: 5
Thread-1 incremented count to: 6
Thread-1 incremented count to: 7
Thread-0 incremented count to: 8
Thread-1 incremented count to: 9
Thread-0 incremented count to: 10
Thread-1 incremented count to: 11
Thread-0 incremented count to: 12
Thread-1 incremented count to: 13
Thread-0 incremented count to: 14
Thread-1 incremented count to: 15
Thread-0 incremented count to: 16
Thread-1 incremented count to: 17
Thread-0 incremented count to: 18
Thread-1 incremented count to: 19
Thread-0 incremented count to: 20
Final counter value: 20

Explanation:

🔹Counter Class

Fields:

Count: The shared counter variable.
Lock: An object used for synchronization.

Methods:

increment(): This method increments the counter. The method is synchronized using the lock object to ensure that only one thread can execute the code inside the synchronized block at a time.
getCount(): Returns the current value of the counter.

🔹CounterTask Class

Implements the Runnable interface and contains the logic to call the increment() method of the Counter class multiple times.

🔹Main Method

  • Creates an instance of the Counter class.
  • Creates two threads, both of which run the CounterTask with the shared Counter instance.
  • Starts both threads and waits for them to finish using join().
  • Prints the final value of the counter.

Key Points:

  • The synchronized block ensures that the count++ operation is atomic and thread-safe.
  • Using join() ensures that the main thread waits for both worker threads to complete before printing the final counter value.
  • Thread.sleep(100) is used to simulate some work being done and to make the output more readable by adding a small delay between increments.

This example demonstrates how synchronization can be used to control access to shared resources in a multithreaded environment, preventing race conditions and ensuring data consistency.

coma

Conclusion

Mastering Java thread lifecycle and states is essential for efficient multithreaded programming. This knowledge enables developers to create responsive applications that optimize system resources, especially in multi-core environments. Understanding thread states and transitions helps manage thread behavior effectively, improving performance and minimizing common concurrency issues like race conditions and deadlocks.

In the ever-evolving landscape of Java development, proficiency in thread management is a crucial skill. Proper implementation of thread lifecycle management and synchronization techniques allows developers to build scalable, efficient applications capable of handling complex tasks and high user loads. As software systems grow more complex and distributed, expertise in Java’s threading model remains vital for creating high-performance, concurrent applications across various domains.

Keep Reading

Keep Reading

  • Service
  • Career
  • Let's create something together!

  • We’re looking for the best. Are you in?