Skip to main content
  1. Resources/
  2. Study Materials/
  3. Information & Communication Technology Engineering/
  4. ICT Semester 4/
  5. Java Programming (4343203)/

11 mins· ·
Milav Dabgar
Author
Milav Dabgar
Experienced lecturer in the electrical and electronic manufacturing industry. Skilled in Embedded Systems, Image Processing, Data Science, MATLAB, Python, STM32. Strong education professional with a Master’s degree in Communication Systems Engineering from L.D. College of Engineering - Ahmedabad.
Java Programming - Multithreading

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

Java 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

AspectThreadProcess
MemoryShared memory spaceSeparate memory space
Creation CostLow (lightweight)High (heavyweight)
CommunicationDirect (shared variables)IPC mechanisms required
Context SwitchingFastSlow
IndependenceNot independentIndependent
Crash ImpactAffects entire processIsolated

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!