Java Multithreading - Complete Tutorial
Master Java Multithreading: Learn Thread, Runnable, Executors, ThreadPool, Synchronization, Locks, Concurrent Collections, CompletableFuture, and best practices for high-performance applications.
What is Multithreading?
Multithreading allows concurrent execution of multiple threads within a single program. Threads share the same memory space but execute independently.
Why Use Multithreading?
- Better CPU utilization — Keep CPU busy while one thread waits for I/O
- Improved responsiveness — UI remains responsive during background tasks
- Faster processing — Parallel execution on multi-core processors
- Resource sharing — Threads share memory more efficiently than processes
Thread or Runnable; synchronize shared mutable state.
Thread Lifecycle
A thread can be in one of six states during its lifetime:
NEW → RUNNABLE → RUNNING → BLOCKED / WAITING / TIMED_WAITING → TERMINATED
| State | Description |
|---|---|
NEW | Thread created but not started |
RUNNABLE | Ready to run, waiting for CPU |
BLOCKED | Waiting for monitor lock |
WAITING | Waiting indefinitely for another thread |
TIMED_WAITING | Waiting for specified time |
TERMINATED | Thread execution completed |
Creating Threads
Method 1: Extending Thread Class
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread " + Thread.currentThread().getName() + " is running");
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ": Count " + i);
try {
Thread.sleep(500); // Pause for 500ms
} catch (InterruptedException e) {
System.out.println("Thread interrupted");
}
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("Thread-A");
t2.setName("Thread-B");
t1.start(); // Starts thread execution
t2.start();
}
}
Method 2: Implementing Runnable Interface (Recommended)
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Running in " + Thread.currentThread().getName());
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " - Iteration " + i);
}
}
public static void main(String[] args) {
// Create Runnable objects
MyRunnable task1 = new MyRunnable();
MyRunnable task2 = new MyRunnable();
// Create threads with Runnable
Thread thread1 = new Thread(task1, "Worker-1");
Thread thread2 = new Thread(task2, "Worker-2");
thread1.start();
thread2.start();
// Using lambda (Java 8+)
Thread thread3 = new Thread(() -> {
System.out.println("Lambda thread running");
}, "Lambda-Thread");
thread3.start();
}
}
Method 3: Using Lambda Expressions (Java 8+)
public class LambdaThreadExample {
public static void main(String[] args) {
// Simple lambda
Thread t1 = new Thread(() -> {
System.out.println("Thread 1 is running");
});
// Lambda with parameters
Thread t2 = new Thread(() -> {
for (int i = 1; i <= 3; i++) {
System.out.println("Count: " + i);
try { Thread.sleep(100); }
catch (InterruptedException e) {}
}
});
t1.start();
t2.start();
}
}
Method 4: Using Callable and Future (Return values)
import java.util.concurrent.*;
public class CallableExample {
public static void main(String[] args) throws Exception {
// Create Callable that returns a result
Callable<Integer> task = () -> {
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
Thread.sleep(100);
}
return sum;
};
// Execute Callable
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(task);
// Do other work while task runs
System.out.println("Waiting for result...");
// Get result (blocks until available)
Integer result = future.get();
System.out.println("Sum from 1 to 10 = " + result);
executor.shutdown();
}
}
Thread Methods and Control
Essential Thread Methods
public class ThreadMethodsDemo {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Working: " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("Thread was interrupted!");
return;
}
}
});
// Set thread properties
worker.setName("WorkerThread");
worker.setPriority(Thread.MAX_PRIORITY); // 1-10, default 5
worker.setDaemon(true); // Daemon thread exits when main thread ends
System.out.println("Thread state before start: " + worker.getState());
worker.start();
System.out.println("Thread state after start: " + worker.getState());
// Main thread sleeps
Thread.sleep(1200);
// Interrupt the thread
worker.interrupt();
// Wait for thread to finish
worker.join(); // Main waits for worker to die
System.out.println("Thread state after completion: " + worker.getState());
System.out.println("Is alive? " + worker.isAlive());
}
}
Thread Priority Example
public class PriorityExample {
public static void main(String[] args) {
Thread low = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Low priority: " + i);
}
});
Thread high = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("HIGH priority: " + i);
}
});
low.setPriority(Thread.MIN_PRIORITY); // 1
high.setPriority(Thread.MAX_PRIORITY); // 10
low.start();
high.start();
// Note: Priority is a hint, not guaranteed
}
}
Synchronization and Thread Safety
When multiple threads access shared resources, synchronization prevents data corruption.
Problem: Race Condition
class Counter {
private int count = 0;
public void increment() {
count++; // Not atomic! Three operations: read, increment, write
}
public int getCount() {
return count;
}
}
public class RaceConditionDemo {
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("Expected: 2000, Actual: " + counter.getCount());
// Output might be less than 2000 due to race condition!
}
}
Solution 1: Synchronized Method
class SynchronizedCounter {
private int count = 0;
public synchronized void increment() {
count++; // Only one thread can execute this at a time
}
public synchronized int getCount() {
return count;
}
}
// Now the result will always be 2000
Solution 2: Synchronized Block
class SynchronizedBlockCounter {
private int count = 0;
private final Object lock = new Object(); // Lock object
public void increment() {
synchronized(lock) {
count++;
}
}
public int getCount() {
synchronized(lock) {
return count;
}
}
}
Solution 3: Volatile Keyword
class VolatileExample {
private volatile boolean running = true; // Changes visible to all threads
public void run() {
while (running) {
// Do work
}
}
public void stop() {
running = false; // Immediately visible to other threads
}
}
Practical Example: Bank Account
class BankAccount {
private double balance;
private final Object lock = new Object();
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
public void deposit(double amount) {
synchronized(lock) {
balance += amount;
System.out.println(Thread.currentThread().getName() +
" deposited " + amount +
", balance: " + balance);
}
}
public boolean withdraw(double amount) {
synchronized(lock) {
if (balance >= amount) {
balance -= amount;
System.out.println(Thread.currentThread().getName() +
" withdrew " + amount +
", balance: " + balance);
return true;
}
System.out.println(Thread.currentThread().getName() +
" insufficient funds!");
return false;
}
}
public double getBalance() {
synchronized(lock) {
return balance;
}
}
}
public class BankDemo {
public static void main(String[] args) throws InterruptedException {
BankAccount account = new BankAccount(1000);
// Multiple threads accessing same account
Thread user1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
account.deposit(100);
try { Thread.sleep(50); } catch (InterruptedException e) {}
}
}, "User-1");
Thread user2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
account.withdraw(150);
try { Thread.sleep(50); } catch (InterruptedException e) {}
}
}, "User-2");
user1.start();
user2.start();
user1.join();
user2.join();
System.out.println("Final balance: " + account.getBalance());
}
}
Inter-thread Communication
wait(), notify(), and notifyAll() coordinate producer-consumer style communication between threads.
class MessageQueue {
private String message;
private boolean hasMessage = false;
public synchronized void put(String message) {
while (hasMessage) {
try {
wait(); // Wait for consumer to consume
} catch (InterruptedException e) {}
}
this.message = message;
hasMessage = true;
System.out.println("Produced: " + message);
notify(); // Notify waiting consumer
}
public synchronized String take() {
while (!hasMessage) {
try {
wait(); // Wait for producer to produce
} catch (InterruptedException e) {}
}
hasMessage = false;
System.out.println("Consumed: " + message);
notify(); // Notify waiting producer
return message;
}
}
public class ProducerConsumerDemo {
public static void main(String[] args) {
MessageQueue queue = new MessageQueue();
// Producer thread
Thread producer = new Thread(() -> {
String[] messages = {"Hello", "World", "Java", "Multithreading", "Done"};
for (String msg : messages) {
queue.put(msg);
try { Thread.sleep(500); } catch (InterruptedException e) {}
}
});
// Consumer thread
Thread consumer = new Thread(() -> {
for (int i = 0; i < 5; i++) {
String msg = queue.take();
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
});
producer.start();
consumer.start();
}
}
Thread Pools (Executor Framework)
Managing threads manually is inefficient. Use ExecutorService for thread pooling.
Fixed Thread Pool
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// Create a pool of 3 threads
ExecutorService executor = Executors.newFixedThreadPool(3);
// Submit tasks
for (int i = 1; i <= 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " executed by " +
Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
});
}
// Shutdown executor
executor.shutdown();
// Wait for all tasks to complete
try {
executor.awaitTermination(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {}
System.out.println("All tasks completed");
}
}
Different Executor Types
import java.util.concurrent.*;
public class ExecutorTypesDemo {
public static void main(String[] args) {
// 1. Fixed thread pool - Reuses fixed number of threads
ExecutorService fixedPool = Executors.newFixedThreadPool(5);
// 2. Cached thread pool - Creates threads as needed, reuses idle ones
ExecutorService cachedPool = Executors.newCachedThreadPool();
// 3. Single thread executor - One thread, tasks execute sequentially
ExecutorService singlePool = Executors.newSingleThreadExecutor();
// 4. Scheduled thread pool - For delayed/recurring tasks
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2);
// Schedule task with delay
scheduledPool.schedule(() -> {
System.out.println("Delayed task executed");
}, 3, TimeUnit.SECONDS);
// Schedule recurring task
scheduledPool.scheduleAtFixedRate(() -> {
System.out.println("Recurring task");
}, 1, 2, TimeUnit.SECONDS);
// Shutdown after some time
try {
Thread.sleep(10000);
} catch (InterruptedException e) {}
fixedPool.shutdown();
cachedPool.shutdown();
singlePool.shutdown();
scheduledPool.shutdown();
}
}
Concurrency Utilities
CountDownLatch
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3); // Wait for 3 tasks
// Create worker threads
for (int i = 1; i <= 3; i++) {
final int workerId = i;
new Thread(() -> {
System.out.println("Worker " + workerId + " is working");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {}
System.out.println("Worker " + workerId + " finished");
latch.countDown(); // Decrement counter
}).start();
}
System.out.println("Main thread waiting for workers...");
latch.await(); // Wait until count reaches 0
System.out.println("All workers completed! Main thread proceeds.");
}
}
CyclicBarrier
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("All threads reached barrier! Let's proceed.");
});
for (int i = 1; i <= 3; i++) {
final int threadId = i;
new Thread(() -> {
System.out.println("Thread " + threadId + " is working...");
try {
Thread.sleep(threadId * 1000);
System.out.println("Thread " + threadId + " reached barrier");
barrier.await(); // Wait for others
System.out.println("Thread " + threadId + " passed barrier");
} catch (InterruptedException | BrokenBarrierException e) {}
}).start();
}
}
}
Semaphore
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2); // Allow 2 concurrent accesses
for (int i = 1; i <= 5; i++) {
final int threadId = i;
new Thread(() -> {
try {
System.out.println("Thread " + threadId + " waiting for permit");
semaphore.acquire(); // Acquire permit
System.out.println("Thread " + threadId + " acquired permit");
// Critical section
Thread.sleep(2000);
semaphore.release(); // Release permit
System.out.println("Thread " + threadId + " released permit");
} catch (InterruptedException e) {}
}).start();
}
}
}
CompletableFuture (Java 8+)
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) throws Exception {
// Async computation
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Computing in " + Thread.currentThread().getName());
return "Hello";
}).thenApply(result -> {
return result + " World";
}).thenApply(String::toUpperCase);
// Chain multiple operations
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 10)
.thenApply(num -> num * 2)
.thenApply(num -> num + 5);
// Combine two futures
CompletableFuture<String> combined = future.thenCombine(future2,
(str, num) -> str + " - " + num);
System.out.println(combined.get()); // HELLO WORLD - 25
// Handle exceptions
CompletableFuture<Integer> errorFuture = CompletableFuture.supplyAsync(() -> {
if (true) throw new RuntimeException("Error occurred");
return 42;
}).exceptionally(ex -> {
System.out.println("Error: " + ex.getMessage());
return 0;
});
System.out.println(errorFuture.get()); // 0
}
}
Atomic Variables and Locks
Atomic Variables (java.util.concurrent.atomic)
import java.util.concurrent.atomic.*;
public class AtomicExample {
public static void main(String[] args) throws InterruptedException {
// AtomicInteger - Thread-safe integer operations
AtomicInteger counter = new AtomicInteger(0);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet(); // Atomic increment
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Atomic counter: " + counter.get()); // Always 2000
// Other atomic types
AtomicBoolean flag = new AtomicBoolean(false);
AtomicLong longValue = new AtomicLong(100L);
AtomicReference<String> reference = new AtomicReference<>("Hello");
// Compare and set
reference.compareAndSet("Hello", "World");
System.out.println(reference.get()); // World
// Atomic operations
int oldValue = counter.getAndSet(500);
int updated = counter.addAndGet(100);
System.out.println("Old: " + oldValue + ", Updated: " + updated);
}
}
ReentrantLock
import java.util.concurrent.locks.*;
class LockedCounter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void increment() {
lock.lock();
try {
count++;
condition.signalAll(); // Notify waiting threads
} finally {
lock.unlock(); // Always unlock in finally block
}
}
public void waitForValue(int target) throws InterruptedException {
lock.lock();
try {
while (count < target) {
condition.await(); // Wait for condition
}
System.out.println("Target " + target + " reached!");
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
public class ReentrantLockDemo {
public static void main(String[] args) {
LockedCounter counter = new LockedCounter();
// Thread waiting for counter to reach 5
Thread waiter = new Thread(() -> {
try {
counter.waitForValue(5);
} catch (InterruptedException e) {}
});
// Thread incrementing counter
Thread incrementer = new Thread(() -> {
for (int i = 0; i < 10; i++) {
counter.increment();
System.out.println("Count: " + counter.getCount());
try { Thread.sleep(500); } catch (InterruptedException e) {}
}
});
waiter.start();
incrementer.start();
}
}
Deadlock and Prevention
What is Deadlock?
class DeadlockExample {
private static final Object LOCK1 = new Object();
private static final Object LOCK2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized(LOCK1) {
System.out.println("Thread 1: Holding LOCK1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 1: Waiting for LOCK2");
synchronized(LOCK2) {
System.out.println("Thread 1: Acquired LOCK2");
}
}
});
Thread t2 = new Thread(() -> {
synchronized(LOCK2) {
System.out.println("Thread 2: Holding LOCK2");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 2: Waiting for LOCK1");
synchronized(LOCK1) {
System.out.println("Thread 2: Acquired LOCK1");
}
}
});
t1.start();
t2.start();
// DEADLOCK! Both threads waiting for each other's locks
}
}
Preventing Deadlock
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
class DeadlockPrevention {
private static final Object LOCK1 = new Object();
private static final Object LOCK2 = new Object();
// Solution 1: Always acquire locks in same order
static class Solution1 {
public void method1() {
synchronized(LOCK1) {
synchronized(LOCK2) {
// Safe - same order as method2
}
}
}
public void method2() {
synchronized(LOCK1) {
synchronized(LOCK2) {
// Safe - same order
}
}
}
}
// Solution 2: Use tryLock() with timeout
static class Solution2 {
private final ReentrantLock lock1 = new ReentrantLock();
private final ReentrantLock lock2 = new ReentrantLock();
public void safeMethod() {
boolean locked1 = false;
boolean locked2 = false;
try {
locked1 = lock1.tryLock(1, TimeUnit.SECONDS);
locked2 = lock2.tryLock(1, TimeUnit.SECONDS);
if (locked1 && locked2) {
// Perform operations
}
} catch (InterruptedException e) {
System.out.println("Could not acquire locks");
} finally {
if (locked1) lock1.unlock();
if (locked2) lock2.unlock();
}
}
}
}
Command Line Arguments in Multithreading
Pass command-line arguments to configure multithreaded applications:
import java.util.concurrent.*;
public class ThreadConfigFromArgs {
static class Config {
int numThreads;
int taskCount;
int delayMs;
Config(String[] args) {
// Parse command line arguments
this.numThreads = args.length > 0 ? Integer.parseInt(args[0]) : 4;
this.taskCount = args.length > 1 ? Integer.parseInt(args[1]) : 20;
this.delayMs = args.length > 2 ? Integer.parseInt(args[2]) : 100;
}
}
public static void main(String[] args) {
Config config = new Config(args);
System.out.println("Configuration:");
System.out.println("Threads: " + config.numThreads);
System.out.println("Tasks: " + config.taskCount);
System.out.println("Delay: " + config.delayMs + "ms");
// Create thread pool based on config
ExecutorService executor = Executors.newFixedThreadPool(config.numThreads);
// Submit tasks
for (int i = 0; i < config.taskCount; i++) {
final int taskId = i;
executor.submit(() -> {
try {
System.out.println("Task " + taskId + " started by " +
Thread.currentThread().getName());
Thread.sleep(config.delayMs);
System.out.println("Task " + taskId + " completed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
executor.shutdown();
try {
executor.awaitTermination(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("All tasks completed");
}
}
Running with arguments:
java ThreadConfigFromArgs 8 100 50
# 8 threads, 100 tasks, 50ms delay per task
Real-World Examples
This tutorial includes several patterns you will see in production code:
| Example | Use Case | Section |
|---|---|---|
BankDemo |
Thread-safe shared resource (bank account deposits and withdrawals) | Synchronization |
ProducerConsumerDemo |
Producer-consumer coordination with wait() / notify() |
Inter-thread Communication |
ThreadConfigFromArgs |
Configurable thread pool driven by command-line arguments | Command Line Arguments |
Together, these examples show how to protect shared state, coordinate work between threads, and scale concurrency settings without changing source code.
Best Practices and Common Pitfalls
DO's
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
import java.util.*;
// 1. Always use thread-safe collections
Map<String, String> map = new ConcurrentHashMap<>();
List<String> list = new CopyOnWriteArrayList<>();
Queue<String> queue = new ConcurrentLinkedQueue<>();
// 2. Use ExecutorService instead of raw threads
ExecutorService executor = Executors.newFixedThreadPool(10);
// 3. Always release locks in finally block
Lock lock = new ReentrantLock();
lock.lock();
try {
// critical section
} finally {
lock.unlock();
}
// 4. Use volatile for flags
private volatile boolean running = true;
// 5. Prefer higher-level concurrency utilities
CountDownLatch latch = new CountDownLatch(3);
DON'Ts
// 1. Don't use Thread.stop() (deprecated and unsafe)
// Thread.stop(); // DON'T DO THIS
// 2. Don't ignore InterruptedException
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// DON'T just print - restore interrupted status
Thread.currentThread().interrupt();
}
// 3. Don't synchronize on non-final objects
String s = "hello";
synchronized(s) { // BAD - s might change
// ...
}
// 4. Don't call Thread.sleep() in synchronized blocks
synchronized(lock) {
Thread.sleep(1000); // BAD - holding lock while sleeping
}
// 5. Don't use Vector or Hashtable (use concurrent collections instead)
// Vector<String> v = new Vector<>(); // Consider ArrayList with synchronization
Summary Table
| Concept | When to Use | Example |
|---|---|---|
Thread | Simple background tasks | new Thread(() -> {}).start() |
Runnable | Task that doesn't return result | executor.submit(() -> {}) |
Callable | Task that returns result | Future<T> result = executor.submit(callable) |
synchronized | Simple mutual exclusion | synchronized(this) { } |
ReentrantLock | Advanced locking (tryLock, conditions) | lock.lock(); try {} finally { lock.unlock(); } |
ExecutorService | Managing thread pools | Executors.newFixedThreadPool(10) |
BlockingQueue | Producer-consumer patterns | new ArrayBlockingQueue<>(5) |
CountDownLatch | Waiting for multiple threads | latch.await(); latch.countDown() |
AtomicInteger | Simple counters | counter.incrementAndGet() |
CompletableFuture | Asynchronous pipelines | future.thenApply().thenAccept() |