Of course! Creating a new thread in Java is a fundamental concept. Let's break it down, from the classic, traditional ways to the modern, recommended approaches.
The Core Concept
In Java, a thread is the smallest unit of a process. It represents a separate path of execution within your program. By creating multiple threads, you can perform several tasks concurrently (or in parallel, depending on the number of CPU cores).
The Classic Ways (Pre-Java 8)
These are the original methods for creating threads. They are still valid and you will see them in older codebases, but they are generally not the preferred approach for new code today.
Method 1: Extending the Thread Class
You create a new class that extends java.lang.Thread and override its run() method. The run() method contains the code that the thread will execute.
Steps:
- Create a class that extends
Thread. - Override the
run()method. - Create an instance of your new class.
- Call the
start()method on the instance. (This is crucial! Callingrun()directly will just execute the code in the current thread, not a new one.)
Example:
// Step 1: Create a class that extends Thread
class MyThread extends Thread {
// Step 2: Override the run() method
@Override
public void run() {
// Code to be executed by the new thread
for (int i = 1; i <= 5; i++) {
System.out.println("MyThread: " + i);
try {
// Sleep for a bit to simulate work
Thread.sleep(500); // 500 milliseconds
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadExample {
public static void main(String[] args) {
// Step 3: Create an instance of the thread
MyThread thread1 = new MyThread();
// Step 4: Start the thread
thread1.start();
// The main thread continues its execution
for (int i = 1; i <= 5; i++) {
System.out.println("Main Thread: " + i);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Pros:
- Simple and straightforward for basic tasks.
Cons:
- Poor OOP Design: Your class is now a
Thread. What if your class also needs to extend another class? Java doesn't support multiple inheritance, so you can't. - Tight Coupling: Your task (the code in
run()) is tightly coupled to theThreadclass itself.
Method 2: Implementing the Runnable Interface (More Flexible)
This is a more common and flexible approach. You create a class that implements the java.lang.Runnable interface. The Runnable interface is just a "task" that can be executed by a thread.
Steps:
- Create a class that implements
Runnable. - Implement the
run()method. - Create an instance of your
Runnableclass. - Create a
Threadobject, passing yourRunnableinstance to its constructor. - Call
start()on theThreadobject.
Example:
// Step 1: Create a class that implements Runnable
class MyRunnableTask implements Runnable {
// Step 2: Implement the run() method
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("MyRunnable: " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class RunnableExample {
public static void main(String[] args) {
// Step 3: Create an instance of the task
MyRunnableTask myTask = new MyRunnableTask();
// Step 4: Create a Thread, passing the task to it
Thread thread2 = new Thread(myTask);
// Step 5: Start the thread
thread2.start();
// Main thread continues
for (int i = 1; i <= 5; i++) {
System.out.println("Main Thread: " + i);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Pros:
- Better OOP Design: Your class is no longer a
Thread; it's just aRunnabletask. It can still extend another class. - Separation of Concerns: It cleanly separates the "what to do" (the
Runnabletask) from the "how to do it" (theThreadthat executes it).
Cons:
- Slightly more boilerplate code (you need to create a
Threadobject separately).
The Modern Way (Java 8 and Later)
Java 8 introduced Lambdas, which made the Runnable approach much cleaner and more concise. This is now the recommended standard for most new code.
You can pass a lambda expression directly to the Thread constructor. A lambda is a short, anonymous block of code that implements a functional interface (like Runnable).
Example:
public class LambdaExample {
public static void main(String[] args) {
// Create a Thread and pass a lambda expression that implements the run() method
Thread thread3 = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Lambda Thread: " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread3.start();
// Main thread continues
for (int i = 1; i <= 5; i++) {
System.out.println("Main Thread: " + i);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Why is this better?
- Conciseness: No need to create a separate class. The task is defined right where it's needed.
- Readability: It's very clear that you're defining a task to be run on a new thread.
- Modern Java Practice: This aligns with functional programming paradigms common in modern Java development.
The Enterprise Way: The ExecutorService Framework
For any real-world application, manually creating and managing Thread objects is not recommended. It's inefficient and error-prone. You should use the Executor Framework, which is part of java.util.concurrent.
The ExecutorService manages a pool of threads for you. This approach provides significant benefits:
- Thread Reuse: It avoids the overhead of creating and destroying threads for every task.
- Resource Management: It prevents you from overwhelming the system by creating too many threads.
- Lifecycle Management: It provides clean ways to shut down the thread pool gracefully.
- Advanced Features: It supports features like scheduled execution, futures, and more.
Example:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ExecutorServiceExample {
public static void main(String[] args) {
// Create a thread pool with 2 threads
ExecutorService executor = Executors.newFixedThreadPool(2);
// Submit tasks to the executor
executor.submit(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Task 1: " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
executor.submit(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Task 2: " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// Shut down the executor. It will not accept new tasks.
executor.shutdown();
try {
// Wait a maximum of 1 second for existing tasks to terminate
if (!executor.awaitTermination(1, TimeUnit.SECONDS)) {
// Forcefully shutdown if tasks are still running
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
Summary and Recommendation
| Method | When to Use | Pros | Cons |
|---|---|---|---|
Extends Thread |
Almost never. Only for very simple, self-contained examples. | Simple for beginners. | Poor OOP design, tight coupling. |
Implements Runnable |
Good for reusable tasks in Java 7 and earlier. | Flexible, good OOP design. | More boilerplate code. |
| Lambda Expression | Recommended for most new code. Use when you need a simple, one-off task. | Concise, readable, modern. | Still requires manual thread management for the task itself. |
ExecutorService |
The standard for all production applications. Use for any non-trivial, concurrent work. | Manages thread pools, efficient, scalable, robust. | Slightly more complex to set up. |
Final Recommendation:
- For learning, start with the
Runnableinterface to understand the concept. - For any practical, modern Java code, use Lambda Expressions with the
ExecutorServiceframework. This gives you the best of both worlds: the conciseness of lambdas and the power and safety of a managed thread pool.
