Java Multithreading
Multithreading allows a program to execute multiple tasks simultaneously within the same process. Each task runs in its own thread — a lightweight unit of execution. Java has built-in support for multithreading through the java.lang.Thread class and the Runnable interface.
Key Concepts
- Process: A running program with its own memory space.
- Thread: A single path of execution within a process. A process can have multiple threads sharing the same memory.
- Multithreading: Running multiple threads simultaneously to improve performance and responsiveness.
Real-World Analogy
Think of a restaurant kitchen. One chef handles desserts, another handles main courses, and a third handles salads — all working simultaneously. Multithreading is the same idea: multiple threads performing different tasks at the same time within one program.
Thread Life Cycle
A thread goes through several states during its lifetime:
- New: Thread object created, but not started yet.
- Runnable:
start()called; thread is ready to run. - Running: Thread is executing.
- Blocked/Waiting: Thread is paused (waiting for a resource or another thread).
- Terminated: Thread has completed execution.
Creating Threads – Two Ways
Method 1 – Extending the Thread Class
Create a class that extends Thread and override the run() method with the task to be performed.
class PrintNumbers extends Thread {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(getName() + " → " + i);
try {
Thread.sleep(100); // pause for 100ms
} catch (InterruptedException e) {
System.out.println("Thread interrupted.");
}
}
}
}
public class Main {
public static void main(String[] args) {
PrintNumbers t1 = new PrintNumbers();
PrintNumbers t2 = new PrintNumbers();
t1.setName("Thread-A");
t2.setName("Thread-B");
t1.start(); // starts the thread
t2.start();
}
}Sample Output (order may vary each run):
Thread-A → 1
Thread-B → 1
Thread-A → 2
Thread-B → 2
...Method 2 – Implementing the Runnable Interface (Preferred)
Implementing Runnable is preferred because Java supports only single inheritance. Using Runnable keeps the option to extend another class.
class DownloadTask implements Runnable {
private String fileName;
DownloadTask(String fileName) {
this.fileName = fileName;
}
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println("Downloading " + fileName + " – " + (i * 33) + "% complete");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println(fileName + " download complete.");
}
}
public class Main {
public static void main(String[] args) {
Thread t1 = new Thread(new DownloadTask("report.pdf"));
Thread t2 = new Thread(new DownloadTask("photo.jpg"));
t1.start();
t2.start();
}
}Thread Sleep
Thread.sleep(milliseconds) pauses the current thread for the specified amount of time. It throws InterruptedException, which must be handled.
try {
Thread.sleep(1000); // sleep for 1 second
} catch (InterruptedException e) {
e.printStackTrace();
}Thread Priority
Threads can be assigned a priority (1 to 10) that suggests to the JVM which thread should run first. The JVM scheduler is not guaranteed to follow priority strictly.
Thread t1 = new Thread(task1);
Thread t2 = new Thread(task2);
t1.setPriority(Thread.MAX_PRIORITY); // 10
t2.setPriority(Thread.MIN_PRIORITY); // 1
t1.start();
t2.start();Synchronization
When multiple threads access shared data, problems can occur — one thread may read a value while another is modifying it, causing inconsistent results. This is called a race condition.
The synchronized keyword ensures that only one thread can access a method or block at a time.
Without Synchronization (Problem)
class Counter {
int count = 0;
void increment() {
count++; // not thread-safe
}
}With Synchronization (Solution)
class Counter {
int count = 0;
synchronized void increment() {
count++; // only one thread can execute this at a time
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) counter.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) counter.increment();
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + counter.count); // 2000
}
}The join() Method
join() makes the calling thread wait until the specified thread finishes its execution before continuing. Useful to ensure a thread completes before reading its results.
t1.start();
t1.join(); // main thread waits here until t1 finishes
System.out.println("t1 has finished.");Lambda Threads (Java 8+)
With lambda expressions, creating a thread becomes more concise:
Thread t = new Thread(() -> {
System.out.println("Running in a lambda thread.");
});
t.start();Executor Framework (Modern Approach)
The ExecutorService from java.util.concurrent manages a pool of threads — a more scalable and controlled way to handle concurrent tasks.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 5; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName());
});
}
executor.shutdown();
}
}Summary
- Multithreading allows multiple tasks to run concurrently within one program.
- Threads are created by extending
Threador implementingRunnable. - Implementing
Runnableis preferred as it supports better design flexibility. Thread.sleep()pauses a thread;join()waits for a thread to finish.- The
synchronizedkeyword prevents race conditions on shared data. - The
ExecutorServiceprovides a managed, scalable way to run thread pools.
