The Ultimate Guide to Java Concurrency (Thread Creation and Lifecycle)
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 likestart
,sleep
, andinterrupt
, which can be directly used within the class without needing an additionalThread
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:
- New: A thread is in the new state if you create an instance of the
Thread
class but have not yet started it using thestart
method. - 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. - 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.
- Waiting: A thread that is waiting indefinitely for another thread to perform a particular action is in this state. Methods like
Object.wait()
orThread.join()
can cause a thread to enter this state. - 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()
orObject.wait(long timeout)
cause a thread to enter this state. - 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
andthread2
are created but not started.thread2
is started and enters therun()
method, sleeping for 2000ms.thread1
is checked and isNEW
.thread1
is started and enters therun()
method, sleeping for 2000ms.- The main thread sleeps for 100ms, and
thread1
isTIMED_WAITING
. - The main thread sleeps for 2000ms, and
thread1
is likelyBLOCKED
due tothread2
holding the lock. - The main thread waits for
thread1
(due to thejoin
method, which we will discuss in the next blog) to finish, resulting in theTERMINATED
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!