Java Programming Language
Chapter 10: Multithreading
Concurrent Programming in Java
Course: 4343203 - Java Programming
What We'll Cover
- Thread Fundamentals
- Creating Threads
- Thread Lifecycle
- Thread Synchronization
- Inter-thread Communication
- Thread Pool and Executor Framework
- Common Threading Issues
- Best Practices
Thread Lifecycle
Thread Fundamentals
Thread is a lightweight subprocess that allows concurrent execution
What is a Thread?
Thread Characteristics:
- Independent execution path within a process
- Shares memory space with other threads
- Has its own stack and program counter
- Lighter weight than processes
- Enables concurrent and parallel execution
Single-threaded vs Multi-threaded:
// Single-threaded approach
public class SingleThreaded {
public static void main(String[] args) {
// Task 1 - must complete before Task 2
for (int i = 0; i < 5; i++) {
System.out.println("Task 1: " + i);
try {
Thread.sleep(1000); // Simulate work
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// Task 2 - starts only after Task 1 completes
for (int i = 0; i < 5; i++) {
System.out.println("Task 2: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// Total time: ~10 seconds
Benefits of Multithreading:
Performance:
- Parallel execution on multi-core systems
- Better CPU utilization
- Reduced overall execution time
Responsiveness:
- User interface remains responsive
- Background processing
- Non-blocking operations
Resource Sharing:
- Threads share memory space
- Lower overhead than processes
- Efficient communication
Thread vs Process
| Aspect | Thread | Process |
|---|---|---|
| Memory | Shared memory space | Separate memory space |
| Creation Cost | Low (lightweight) | High (heavyweight) |
| Communication | Direct (shared variables) | IPC mechanisms required |
| Context Switching | Fast | Slow |
| Independence | Not independent | Independent |
| Crash Impact | Affects entire process | Isolated |
Creating Threads
Java provides multiple ways to create and start threads
Method 1: Extending Thread Class
Basic Thread Extension:
class MyThread extends Thread {
private String threadName;
public MyThread(String name) {
this.threadName = name;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(threadName + ": Count " + i);
try {
Thread.sleep(1000); // Pause for 1 second
} catch (InterruptedException e) {
System.out.println(threadName + " interrupted");
return;
}
}
System.out.println(threadName + " finished");
}
}
public class ThreadExample1 {
public static void main(String[] args) {
MyThread thread1 = new MyThread("Thread-1");
MyThread thread2 = new MyThread("Thread-2");
thread1.start(); // Start first thread
thread2.start(); // Start second thread
System.out.println("Main thread continues...");
}
}
Output (may vary):
Main thread continues...
Thread-1: Count 1
Thread-2: Count 1
Thread-1: Count 2
Thread-2: Count 2
Thread-1: Count 3
Thread-2: Count 3
Thread-1: Count 4
Thread-2: Count 4
Thread-1: Count 5
Thread-2: Count 5
Thread-1 finished
Thread-2 finished
Key Points:
- Override run() method
- Call start() to begin execution
- Never call run() directly
- Threads execute concurrently
Method 2: Implementing Runnable Interface
Runnable Implementation:
class MyTask implements Runnable {
private String taskName;
public MyTask(String name) {
this.taskName = name;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(taskName + ": Executing step " + i);
try {
Thread.sleep(800);
} catch (InterruptedException e) {
System.out.println(taskName + " interrupted");
return;
}
}
System.out.println(taskName + " completed");
}
}
public class RunnableExample {
public static void main(String[] args) {
// Create Runnable objects
MyTask task1 = new MyTask("Task-A");
MyTask task2 = new MyTask("Task-B");
// Create Thread objects with Runnable
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
thread1.start();
thread2.start();
}
}
Lambda Expression (Java 8+):
public class LambdaThreadExample {
public static void main(String[] args) {
// Using lambda expression
Thread thread1 = new Thread(() -> {
for (int i = 1; i <= 3; i++) {
System.out.println("Lambda Thread: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// Using method reference
Thread thread2 = new Thread(LambdaThreadExample::performTask);
thread1.start();
thread2.start();
}
public static void performTask() {
for (int i = 1; i <= 3; i++) {
System.out.println("Method Reference Thread: " + i);
try {
Thread.sleep(1200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Thread vs Runnable Comparison
Extending Thread
Pros:
- Simple to use
- Direct access to Thread methods
- No need for Thread wrapper
Cons:
- Cannot extend another class
- Tight coupling with Thread class
- Less flexible
Implementing Runnable
Pros:
- Can extend another class
- Better separation of concerns
- More flexible and reusable
- Preferred approach
Cons:
- Requires Thread wrapper
- Slightly more verbose
Thread Lifecycle and States
Threads go through various states during their execution
Thread States
NEW
Thread created but not started
Thread t = new Thread();
// State: NEW
RUNNABLE
Thread executing or ready to execute
t.start();
// State: RUNNABLE
BLOCKED
Waiting for monitor lock
synchronized(obj) {
// Another thread holds lock
// State: BLOCKED
}
WAITING
Waiting indefinitely for another thread
obj.wait();
// State: WAITING
TIMED_WAITING
Waiting for specified time
Thread.sleep(1000);
// State: TIMED_WAITING
TERMINATED
Thread execution completed
// run() method finished
// State: TERMINATED
Thread Methods
Common Thread Methods:
public class ThreadMethodsExample {
public static void main(String[] args) {
Thread worker = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
System.out.println("Working: " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("Worker interrupted");
return; // Exit gracefully
}
}
});
System.out.println("State: " + worker.getState()); // NEW
worker.start();
System.out.println("State: " + worker.getState()); // RUNNABLE
try {
Thread.sleep(2000); // Let worker run for 2 seconds
worker.interrupt(); // Send interrupt signal
worker.join(); // Wait for worker to finish
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("State: " + worker.getState()); // TERMINATED
System.out.println("Is alive: " + worker.isAlive()); // false
}
}
Thread Priority:
public class ThreadPriorityExample {
public static void main(String[] args) {
Thread highPriority = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("High Priority: " + i);
}
});
Thread lowPriority = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Low Priority: " + i);
}
});
// Set priorities (1-10, default is 5)
highPriority.setPriority(Thread.MAX_PRIORITY); // 10
lowPriority.setPriority(Thread.MIN_PRIORITY); // 1
lowPriority.start();
highPriority.start();
// Join threads
try {
highPriority.join();
lowPriority.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Thread Synchronization
Synchronization prevents race conditions and ensures thread safety
Race Condition Problem
Unsynchronized Counter:
class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // Not atomic operation!
// 1. Read count
// 2. Add 1
// 3. Write back
}
public int getCount() {
return count;
}
}
public class RaceConditionExample {
public static void main(String[] args) {
UnsafeCounter counter = new UnsafeCounter();
// Create multiple threads incrementing counter
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
threads[i].start();
}
// Wait for all threads to complete
for (Thread t : threads) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Expected: 10000");
System.out.println("Actual: " + counter.getCount());
// Output might be less than 10000 due to race condition
}
}
Synchronized Solution:
class SafeCounter {
private int count = 0;
// Method-level synchronization
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
// Alternative: Block-level synchronization
class SafeCounter2 {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized(lock) {
count++;
}
}
public int getCount() {
synchronized(lock) {
return count;
}
}
}
public class SynchronizedExample {
public static void main(String[] args) {
SafeCounter counter = new SafeCounter();
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
threads[i].start();
}
for (Thread t : threads) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Count: " + counter.getCount());
// Output: Always 10000
}
}
Synchronization Mechanisms
synchronized keyword
- Method-level synchronization
- Block-level synchronization
- Uses intrinsic locks (monitors)
- Automatic lock acquisition/release
volatile keyword:
class VolatileExample {
private volatile boolean flag = false;
public void writer() {
flag = true; // Visible to all threads immediately
}
public void reader() {
if (flag) {
// Will see the updated value
System.out.println("Flag is true");
}
}
}
Lock Interface (java.util.concurrent)
- More flexible than synchronized
- Explicit lock/unlock operations
- Trylock with timeout
- Interruptible lock acquisition
ReentrantLock Example:
import java.util.concurrent.locks.ReentrantLock;
class LockCounter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // Always unlock in finally
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
Inter-thread Communication
wait(), notify(), notifyAll() enable threads to communicate
Producer-Consumer Example
import java.util.LinkedList;
import java.util.Queue;
class ProducerConsumer {
private final Queue queue = new LinkedList<>();
private final int CAPACITY = 5;
public synchronized void produce(int item) throws InterruptedException {
while (queue.size() == CAPACITY) {
System.out.println("Queue is full, producer waiting...");
wait(); // Release lock and wait
}
queue.offer(item);
System.out.println("Produced: " + item + ", Queue size: " + queue.size());
notifyAll(); // Notify waiting consumers
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
System.out.println("Queue is empty, consumer waiting...");
wait(); // Release lock and wait
}
int item = queue.poll();
System.out.println("Consumed: " + item + ", Queue size: " + queue.size());
notifyAll(); // Notify waiting producers
return item;
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer();
// Producer thread
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
pc.produce(i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// Consumer thread
Thread consumer = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
pc.consume();
Thread.sleep(150);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.start();
consumer.start();
try {
producer.join();
consumer.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Thread Pool and Executor Framework
Executor Framework provides high-level thread management
Using ExecutorService
Fixed Thread Pool:
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// Create thread pool with 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(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// Shutdown executor
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
}
Different Executor Types:
// Fixed thread pool
ExecutorService fixed =
Executors.newFixedThreadPool(4);
// Cached thread pool (dynamic sizing)
ExecutorService cached =
Executors.newCachedThreadPool();
// Single thread executor
ExecutorService single =
Executors.newSingleThreadExecutor();
// Scheduled executor
ScheduledExecutorService scheduled =
Executors.newScheduledThreadPool(2);
// Schedule task with delay
scheduled.schedule(() -> {
System.out.println("Delayed task executed");
}, 5, TimeUnit.SECONDS);
// Schedule recurring task
scheduled.scheduleAtFixedRate(() -> {
System.out.println("Recurring task: " +
new Date());
}, 0, 2, TimeUnit.SECONDS);
Common Threading Issues
Deadlock Example and Prevention
❌ Deadlock Scenario:
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized(lock1) {
System.out.println("Thread 1: Holding lock 1");
try { Thread.sleep(100); }
catch (InterruptedException e) {}
System.out.println("Thread 1: Waiting for lock 2");
synchronized(lock2) {
System.out.println("Thread 1: Holding both locks");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized(lock2) {
System.out.println("Thread 2: Holding lock 2");
try { Thread.sleep(100); }
catch (InterruptedException e) {}
System.out.println("Thread 2: Waiting for lock 1");
synchronized(lock1) {
System.out.println("Thread 2: Holding both locks");
}
}
});
thread1.start();
thread2.start();
// Both threads will wait forever!
}
}
✅ Deadlock Prevention:
public class DeadlockPrevention {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
acquireLocksInOrder(lock1, lock2, "Thread 1");
});
Thread thread2 = new Thread(() -> {
acquireLocksInOrder(lock1, lock2, "Thread 2");
});
thread1.start();
thread2.start();
}
// Always acquire locks in the same order
private static void acquireLocksInOrder(
Object firstLock, Object secondLock, String threadName) {
synchronized(firstLock) {
System.out.println(threadName + ": Holding first lock");
try { Thread.sleep(100); }
catch (InterruptedException e) {}
synchronized(secondLock) {
System.out.println(threadName + ": Holding both locks");
}
}
}
}
Best Practices
✅ Do's
- Use thread pools instead of creating threads manually
- Prefer Runnable over extending Thread
- Use concurrent collections (ConcurrentHashMap)
- Handle InterruptedException properly
- Use AtomicXXX classes for simple operations
- Always release locks in finally blocks
- Use volatile for simple flags
❌ Don'ts
- Don't call Thread.stop(), suspend(), or resume()
- Don't ignore InterruptedException
- Don't synchronize on public objects
- Don't hold locks for too long
- Don't create too many threads
- Don't use busy waiting (polling)
- Don't forget to shutdown ExecutorService
Chapter Summary
Thread Concepts:
- Thread creation (extend Thread vs implement Runnable)
- Thread lifecycle and states
- Thread synchronization mechanisms
- Inter-thread communication
- ExecutorService and thread pools
Practical Skills:
- Creating and managing threads safely
- Preventing race conditions
- Using synchronized blocks and methods
- Implementing producer-consumer patterns
- Avoiding deadlocks and other threading issues
Next: File Handling
Thank You!
Questions?
Ready to explore File Handling!

