What are Java Threads?
A Thread in Java is a single unit of execution within a process. It helps execute multiple tasks concurrently. Java provides in-built support for multithreading using the Thread class and the Runnable interface. Every Java program has at least one thread (the main thread), and additional threads can be created for parallel execution.
Two Ways to Create a Thread in Java:
1. Extending Thread class
class MyThread extends Thread {
public void run() {
System.out.println("Thread " + Thread.currentThread().getId() + " is running...");
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start(); // starts the thread
t2.start();
}
}
2. Implementing Runnable interface
class MyRunnable implements Runnable {
public void run() {
System.out.println("Runnable thread " + Thread.currentThread().getId() + " is running...");
}
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());
Thread t2 = new Thread(new MyRunnable());
t1.start();
t2.start();
}
}
🎯 Objectives
Perform multiple tasks simultaneously
Utilize CPU efficiently by sharing resources
Improve overall application performance
Create responsive user interfaces (background tasks)
✅ Advantages
Better CPU utilization and faster task execution
Improved application responsiveness
Resource sharing between threads
Simplifies modeling of concurrent systems
⚠️ Limitations
Increased programming complexity
Can cause deadlocks, race conditions, and synchronization issues
Difficult debugging and testing
Thread overhead for large number of threads
Java Thread Lifecycle States
A thread passes through several stages during its lifetime:
Thread Lifecycle Demonstration
class LifeCycleDemo extends Thread {
public void run() {
System.out.println("Thread state: Running");
try {
Thread.sleep(1000); // moves to Waiting state
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread finishing execution");
}
public static void main(String[] args) throws InterruptedException {
LifeCycleDemo t1 = new LifeCycleDemo();
System.out.println("Thread state: New");
t1.start();
System.out.println("Thread state: Runnable");
t1.join();
System.out.println("Thread state: Terminated");
}
}
Important Thread Methods
Thread Methods Example
class ThreadMethodsDemo extends Thread {
public void run() {
System.out.println("Thread " + getName() + " running with priority " + getPriority());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("Thread interrupted");
}
}
public static void main(String[] args) {
ThreadMethodsDemo t1 = new ThreadMethodsDemo();
ThreadMethodsDemo t2 = new ThreadMethodsDemo();
t1.setName("Thread-1");
t2.setName("Thread-2");
t1.setPriority(Thread.MIN_PRIORITY); // 1
t2.setPriority(Thread.MAX_PRIORITY); // 10
t1.start();
t2.start();
System.out.println("Main thread: " + Thread.currentThread().getName());
System.out.println("t1 is alive? " + t1.isAlive());
}
}
Creating Threads Using Runnable Interface
Instead of extending the Thread class, we can implement the Runnable interface for more flexibility. This approach is preferred because Java supports single inheritance only, but multiple interfaces can be implemented.
Using Runnable Interface
// Define task using Runnable
class Task implements Runnable {
private String taskName;
Task(String name) {
this.taskName = name;
}
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(taskName + " - step " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println(taskName + " interrupted");
}
}
System.out.println(taskName + " completed");
}
}
public class RunnableExample {
public static void main(String[] args) {
// Create tasks
Task task1 = new Task("Download File");
Task task2 = new Task("Process Data");
Task task3 = new Task("Send Email");
// Create threads with tasks
Thread t1 = new Thread(task1);
Thread t2 = new Thread(task2);
Thread t3 = new Thread(task3);
// Start threads
t1.start();
t2.start();
t3.start();
System.out.println("All tasks submitted to threads");
}
}
Output: All three tasks run concurrently, showing interleaved output
Thread Synchronization
When multiple threads access shared resources, synchronization is needed to prevent data inconsistency and race conditions. Java provides synchronized keyword to control access to shared resources.
Synchronization Example
class Counter {
private int count = 0;
// Synchronized method to prevent race condition
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
class CounterThread extends Thread {
private Counter counter;
CounterThread(Counter counter) {
this.counter = counter;
}
public void run() {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
}
}
public class SynchronizationExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// Create 5 threads
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
threads[i] = new CounterThread(counter);
threads[i].start();
}
// Wait for all threads to complete
for (Thread t : threads) {
t.join();
}
System.out.println("Final count: " + counter.getCount());
// Output: 5000 (with synchronization)
}
}
Note: Without synchronization, the final count may be less than 5000 due to race conditions.
Thread Priority in Java
Each thread in Java has a priority. Thread priority ranges from 1 (MIN_PRIORITY) to 10 (MAX_PRIORITY), with default priority 5 (NORM_PRIORITY). Higher priority threads get more CPU time.
class PriorityDemo extends Thread {
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println(getName() + " (Priority: " + getPriority() + ") - Count: " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
}
}
public static void main(String[] args) {
PriorityDemo t1 = new PriorityDemo();
PriorityDemo t2 = new PriorityDemo();
PriorityDemo t3 = new PriorityDemo();
t1.setName("Low Priority Thread");
t2.setName("Normal Priority Thread");
t3.setName("High Priority Thread");
t1.setPriority(Thread.MIN_PRIORITY); // 1
t2.setPriority(Thread.NORM_PRIORITY); // 5
t3.setPriority(Thread.MAX_PRIORITY); // 10
t1.start();
t2.start();
t3.start();
}
}
Note: Priority is a hint to the scheduler; actual execution order depends on the underlying OS.
Best Practices for Java Threads
Use Runnable interface instead of extending Thread class for better flexibility
Use synchronized keyword or Lock interface to prevent race conditions
Prefer Executor framework (ThreadPoolExecutor) for managing multiple threads
Avoid unnecessary thread creation; use thread pools for large number of short-lived tasks
Always handle InterruptedException properly when using sleep() or wait()
Use volatile keyword for variables accessed by multiple threads to ensure visibility
Use join() to wait for thread completion when needed
Prefer ConcurrentHashMap, BlockingQueue over manual synchronization for collections