Lesson 2
Thread Lifecycle and Basic Operations
Thread Lifecycle and Basic Operations

Welcome to the next step in our journey into Java concurrency. In this lesson, we will delve deeply into the lifecycle of a thread and explore some fundamental thread operations. By the end of this lesson, you will have a firm grasp of thread states, lifecycle management, and how to use essential thread methods to control their behavior.

What You'll Learn

In this lesson, you'll explore:

  • The different states in the lifecycle of a thread.
  • Key thread methods such as start(), sleep(), join(), and setPriority().
  • Practical insights into thread priorities and how they affect thread scheduling.

By the end of this lesson, you will understand how to manage thread lifecycles effectively and use different thread operations to build efficient multithreaded applications.

Thread Lifecycle

In Java, a thread can exist in one of several states throughout its lifecycle. Understanding these states is crucial to designing and debugging multithreaded applications effectively:

  1. NEW: The thread is created but not yet started.
  2. RUNNABLE: The thread is ready to run and is waiting for CPU time.
  3. BLOCKED: The thread is waiting for a monitor lock to enter or re-enter a synchronized block/method. For instance, if multiple threads are trying to access the same synchronized method, they may be blocked until they acquire the lock.
  4. WAITING: The thread is waiting indefinitely for another thread to perform a particular action.
  5. TIMED_WAITING: The thread is waiting for another thread to perform a particular action for a specified waiting time.
  6. TERMINATED: The thread has completed its execution, either because it has run to completion or because an exception has occurred that has terminated the run method.

Consider the following simple implementation of the Runnable interface:

Java
1public class RunnableDemo implements Runnable { 2 private String threadName; 3 4 public RunnableDemo(String name) { 5 threadName = name; 6 } 7 8 public void run() { 9 System.out.println("Running " + threadName); 10 } 11}

This RunnableDemo class implements the Runnable interface and overrides the run method to print the thread's name. Now, let's use this class to understand the basic thread operations.

Creating and Starting Threads

When a thread is created, it starts in the NEW state. It transitions to the RUNNABLE state when the start method is called, which internally invokes the run method in a new thread of execution.

Java
1public class Main { 2 public static void main(String[] args) { 3 Thread t1 = new Thread(new RunnableDemo("Thread-1")); 4 Thread t2 = new Thread(new RunnableDemo("Thread-2")); 5 t1.start(); 6 t2.start(); 7 } 8}

In the Main class, we create two threads, t1 and t2, and start them using the start method. It's important to note that calling the run method directly does not create a new thread—it merely executes the run method in the current (main) thread. Always use the start method to create a new thread of execution.

The Sleep Method

The sleep method pauses the execution of the current thread for a specified duration, putting the thread into the TIMED_WAITING state.

Java
1public void run() { 2 try { 3 System.out.println("Running " + threadName); 4 Thread.sleep(1000); // Sleep for 1 second 5 } catch (InterruptedException e) { 6 System.out.println(threadName + " interrupted."); 7 } 8}

It is enclosed in a try-catch block because Thread.sleep can throw an InterruptedException if another thread interrupts the sleeping one. This exception needs to be handled, and the catch block provides a way to manage this interruption and continue program execution.

In the run method, we make the current thread sleep for 1 second, which is useful for simulating a delay in the thread's activity or for scheduling tasks.

The Join Method

The join method pauses the execution of the current thread until the specified thread completes its execution. This allows one thread to wait for another to finish before proceeding. When the join method is called without a timeout, the current thread will enter the WAITING state until the specified thread completes.

Java
1public class Main { 2 public static void main(String[] args) throws InterruptedException { 3 Thread t1 = new Thread(new RunnableDemo("Thread-1")); 4 t1.start(); 5 t1.join(); // Wait for t1 to finish 6 Thread t2 = new Thread(new RunnableDemo("Thread-2")); 7 t2.start(); 8 } 9}

In this example, t1.join() makes the main thread wait until t1 finishes execution before starting t2. This ensures that t2 only starts running after t1 is done.

Since join can throw an InterruptedException if the current thread is interrupted while waiting, we need to either declare throws InterruptedException in the method signature (as shown above) or handle it using a try-catch block. This ensures that the program safely deals with any interruptions that might occur during the thread's waiting period.

Entering the BLOCKED State

The BLOCKED state occurs when a thread is waiting to gain access to a resource that is being used by another thread in a synchronized block or method. We'll learn more about the synchronized keyword in the next unit, but for now, you can think of it as a mechanism that ensures only one thread can execute a particular method or block of code at a time, preventing conflicts in multi-threaded environments.

In this situation, if multiple threads try to execute the same synchronized code, only one thread can proceed, while others are temporarily blocked until the first thread finishes and releases control of the code.

Java
1public class SynchronizedDemo { 2 // This keyword makes sure that only one thread can access this method at a time, 3 // ensuring thread safety for operations within the method 4 public synchronized void printMessage(String message) { 5 System.out.println(message); 6 } 7} 8 9public class Main { 10 public static void main(String[] args) { 11 SynchronizedDemo demo = new SynchronizedDemo(); 12 13 Runnable task = () -> { 14 demo.printMessage("Hello from " + Thread.currentThread().getName()); 15 }; 16 17 Thread t1 = new Thread(task, "Thread-1"); 18 Thread t2 = new Thread(task, "Thread-2"); 19 20 t1.start(); 21 t2.start(); 22 } 23}

In this example, printMessage is a synchronized method. When t1 is executing this method, t2 will enter the BLOCKED state, waiting for t1 to finish before it can execute the same method.

Terminating Threads

A thread moves to the TERMINATED state after its run method completes execution or when an unhandled exception occurs that stops the thread.

Java
1public void run() { 2 System.out.println("Running " + threadName); 3 // When this method ends, the thread will be TERMINATED 4}

In the above RunnableDemo class, once the run method finishes execution, the thread will move into the TERMINATED state. This state indicates that the thread has either completed its task or has stopped due to an exception.

Setting Priority

In Java, thread priorities can be set using the setPriority method, which helps guide the thread scheduler in deciding which thread to execute first. Priorities range from Thread.MIN_PRIORITY (1) to Thread.MAX_PRIORITY (10), with Thread.NORM_PRIORITY (5) as the default. By default, a thread inherits the priority of its creator.

The JVM uses a fixed-priority pre-emptive scheduling algorithm, which usually gives preference to the highest-priority thread. However, sometimes the scheduler may run lower-priority threads to avoid starvation—a situation where high-priority threads keep lower-priority threads from executing.

Java's Thread class provides:

  • getPriority(): Returns the current priority of the thread.
  • setPriority(int priority): Sets the thread's priority to a value between 1 and 10. Any value outside this range will throw an error.

Here's an example:

Java
1public class Main { 2 public static void main(String[] args) { 3 Thread t1 = new Thread(new RunnableDemo("High Priority Thread")); 4 t1.setPriority(Thread.MAX_PRIORITY); 5 6 Thread t2 = new Thread(new RunnableDemo("Normal Priority Thread")); 7 t2.setPriority(Thread.NORM_PRIORITY); 8 9 Thread t3 = new Thread(new RunnableDemo("Low Priority Thread")); 10 t3.setPriority(Thread.MIN_PRIORITY); 11 12 t1.start(); 13 t2.start(); 14 t3.start(); 15 } 16}

In this example, t1 is assigned maximum priority, t2 normal priority, and t3 minimum priority. The actual execution order can vary, as the behavior of thread priorities is platform-dependent. Thus, priorities are a suggestion to the JVM and not a strict guarantee of execution sequence.

The Importance of Thread Lifecycle and Operations

Understanding the lifecycle and operations of threads is crucial for managing concurrent tasks effectively. Here’s why:

  • Efficient Resource Management: Knowing when and how to pause or terminate threads helps in conserving system resources.
  • Improving Performance: Properly prioritizing threads can lead to more efficient execution of important tasks.
  • Avoiding Deadlocks: Techniques such as careful use of join and managing synchronized blocks help in avoiding deadlocks—situations where threads are waiting indefinitely for each other. (We will explore deadlocks in more depth in a future unit.)
  • Real-World Applications: From web servers to database systems, many real-world applications rely on well-managed threads to handle multiple tasks concurrently, making these concepts vital for developing robust applications.

By mastering these thread lifecycle states and operations, you'll be well-equipped to handle complex concurrent programming scenarios, thereby enhancing your software development skills.

Ready to put these concepts into practice? Let’s move to the exercises and get started!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.