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.
In this lesson, you'll explore:
- The different states in the lifecycle of a thread.
- Key thread methods such as
start()
,sleep()
,join()
, andsetPriority()
. - 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.
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:
- NEW: The thread is created but not yet started.
- RUNNABLE: The thread is ready to run and is waiting for CPU time.
- 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.
- WAITING: The thread is waiting indefinitely for another thread to perform a particular action.
- TIMED_WAITING: The thread is waiting for another thread to perform a particular action for a specified waiting time.
- 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:
Java1public 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.
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.
Java1public 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 pauses the execution of the current thread for a specified duration, putting the thread into the TIMED_WAITING state.
Java1public 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 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.
Java1public 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.
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.
Java1public 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.
A thread moves to the TERMINATED state after its run
method completes execution or when an unhandled exception occurs that stops the thread.
Java1public 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.
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:
Java1public 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.
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!