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

17 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.
Lecture 15: Multi-threading Fundamentals | Java Programming (4343203)

Multi-threading Fundamentals

Java Programming (4343203)

Lecture 15

Unit 4: Advanced Java Concepts
GTU Computer Engineering Semester 4

Learning Objectives

  • Understand the concept of multithreading and concurrency
  • Learn thread creation methods in Java
  • Master thread lifecycle and states
  • Implement thread synchronization techniques
  • Handle race conditions and thread safety
  • Solve producer-consumer problems
Focus: Building concurrent applications with proper thread management and synchronization.

Introduction to Multithreading

What is Multithreading?

  • Thread: Lightweight sub-process
  • Multithreading: Concurrent execution of multiple threads
  • Process: Independent execution environment
  • Concurrency: Multiple tasks progressing simultaneously

Benefits of Multithreading

  • Better resource utilization
  • Improved performance
  • Enhanced user experience
  • Parallel processing capabilities

Single-threaded vs Multi-threaded

Single-threaded:
Task1 → Task2 → Task3 → Task4

Multi-threaded:
Thread1: Task1 → Task3
Thread2: Task2 → Task4
Parallel execution

Challenges:
  • Race conditions
  • Deadlocks
  • Resource sharing conflicts
  • Debugging complexity

Thread Creation in Java

Method 1: Extending Thread Class


// Extending Thread class
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); // Sleep for 1 second
            } catch (InterruptedException e) {
                System.out.println(threadName + " interrupted");
                return;
            }
        }
        System.out.println(threadName + " finished execution");
    }
}

// Usage
public class ThreadExample1 {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread("Thread-1");
        MyThread thread2 = new MyThread("Thread-2");
        
        // Start threads
        thread1.start(); // Calls run() method in new thread
        thread2.start();
        
        System.out.println("Main thread continues...");
        
        // Wait for threads to complete
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("All threads completed");
    }
}
                

Method 2: Implementing Runnable Interface


// Implementing Runnable interface (Preferred approach)
class MyRunnable implements Runnable {
    private String taskName;
    private int iterations;
    
    public MyRunnable(String name, int iterations) {
        this.taskName = name;
        this.iterations = iterations;
    }
    
    @Override
    public void run() {
        Thread currentThread = Thread.currentThread();
        System.out.println(taskName + " started on " + currentThread.getName());
        
        for (int i = 1; i <= iterations; i++) {
            System.out.printf("%s: Iteration %d/%d [%s]%n", 
                             taskName, i, iterations, currentThread.getName());
            
            try {
                Thread.sleep(500); // Simulate work
            } catch (InterruptedException e) {
                System.out.println(taskName + " interrupted");
                Thread.currentThread().interrupt(); // Restore interrupt status
                return;
            }
        }
        
        System.out.println(taskName + " completed");
    }
}

// Usage with Thread constructor
public class RunnableExample {
    public static void main(String[] args) {
        // Create Runnable tasks
        MyRunnable task1 = new MyRunnable("Download-Task", 3);
        MyRunnable task2 = new MyRunnable("Upload-Task", 4);
        MyRunnable task3 = new MyRunnable("Process-Task", 2);
        
        // Create threads with Runnable tasks
        Thread thread1 = new Thread(task1);
        Thread thread2 = new Thread(task2);
        Thread thread3 = new Thread(task3, "CustomThread-3");
        
        // Set thread properties
        thread1.setPriority(Thread.MAX_PRIORITY);
        thread2.setPriority(Thread.MIN_PRIORITY);
        
        // Start threads
        thread1.start();
        thread2.start();
        thread3.start();
        
        System.out.println("Main thread ID: " + Thread.currentThread().getId());
    }
}
                

Thread Lifecycle and States

Thread States

StateDescription
NEWThread created but not started
RUNNABLEThread executing or ready to execute
BLOCKEDThread blocked waiting for monitor lock
WAITINGThread waiting indefinitely
TIMED_WAITINGThread waiting for specified time
TERMINATEDThread finished execution

State Transition Methods

  • start() - NEW → RUNNABLE
  • sleep(ms) - RUNNABLE → TIMED_WAITING
  • wait() - RUNNABLE → WAITING
  • join() - Current thread waits
  • notify() - WAITING → RUNNABLE
  • interrupt() - Interrupt waiting thread
Important: Call start() to create new thread. Calling run() directly executes in current thread!

Thread Lifecycle Demonstration


public class ThreadLifecycleDemo {
    public static void main(String[] args) {
        Thread worker = new Thread(new WorkerTask(), "Worker-Thread");
        
        // State monitoring
        System.out.println("1. Initial state: " + worker.getState()); // NEW
        
        worker.start();
        System.out.println("2. After start(): " + worker.getState()); // RUNNABLE
        
        // Let worker thread run for a bit
        try {
            Thread.sleep(100);
            System.out.println("3. During execution: " + worker.getState());
            
            // Wait for worker to complete
            worker.join();
            System.out.println("4. After completion: " + worker.getState()); // TERMINATED
            
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("Main thread finished");
    }
}

class WorkerTask implements Runnable {
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        
        try {
            System.out.println(threadName + " starting work...");
            
            // Simulate some work
            for (int i = 1; i <= 3; i++) {
                System.out.println(threadName + " working... step " + i);
                Thread.sleep(300); // TIMED_WAITING state
            }
            
            System.out.println(threadName + " work completed!");
            
        } catch (InterruptedException e) {
            System.out.println(threadName + " was interrupted");
            Thread.currentThread().interrupt();
        }
    }
}
                

Thread Synchronization

The Race Condition Problem


// Demonstrating race condition
class Counter {
    private int count = 0;
    
    // Unsynchronized method - causes race condition
    public void increment() {
        // This is actually 3 operations:
        // 1. Read current value of count
        // 2. Increment the value
        // 3. Store the new value back to count
        count++; // Not atomic!
    }
    
    // Synchronized method - thread-safe
    public synchronized void synchronizedIncrement() {
        count++;
    }
    
    public int getCount() {
        return count;
    }
    
    public void resetCount() {
        count = 0;
    }
}

// Race condition demonstration
public class RaceConditionDemo {
    private static Counter counter = new Counter();
    
    public static void main(String[] args) {
        // Test unsynchronized increment
        System.out.println("=== Testing Race Condition ===");
        testRaceCondition();
        
        // Test synchronized increment
        System.out.println("\n=== Testing Synchronized Access ===");
        testSynchronizedAccess();
    }
    
    private static void testRaceCondition() {
        counter.resetCount();
        
        Thread[] threads = new Thread[5];
        
        // Create threads that increment counter
        for (int i = 0; i < 5; i++) {
            final int threadId = i;
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter.increment(); // Race condition here!
                }
                System.out.println("Thread " + threadId + " finished");
            });
        }
        
        // Start all threads
        for (Thread thread : threads) {
            thread.start();
        }
        
        // Wait for all threads to complete
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        System.out.println("Expected: 5000, Actual: " + counter.getCount());
        System.out.println("Data corruption due to race condition!");
    }
}
                

Synchronization Mechanisms

1. Synchronized Methods


class BankAccount {
    private double balance;
    private String accountNumber;
    
    public BankAccount(String accountNumber, double initialBalance) {
        this.accountNumber = accountNumber;
        this.balance = initialBalance;
    }
    
    // Synchronized method - only one thread can execute at a time
    public synchronized void deposit(double amount) {
        if (amount > 0) {
            System.out.println(Thread.currentThread().getName() + 
                             " depositing $" + amount);
            double oldBalance = balance;
            
            // Simulate processing time
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }
            
            balance = oldBalance + amount;
            System.out.println("Deposit complete. New balance: $" + balance);
        }
    }
    
    // Synchronized method
    public synchronized void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            System.out.println(Thread.currentThread().getName() + 
                             " withdrawing $" + amount);
            double oldBalance = balance;
            
            // Simulate processing time
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }
            
            balance = oldBalance - amount;
            System.out.println("Withdrawal complete. New balance: $" + balance);
        } else {
            System.out.println("Insufficient funds for withdrawal of $" + amount);
        }
    }
    
    // Synchronized getter
    public synchronized double getBalance() {
        return balance;
    }
}
                

2. Synchronized Blocks


class SharedResource {
    private int data = 0;
    private final Object lock = new Object(); // Explicit lock object
    
    // Method with synchronized block
    public void updateData(int newValue) {
        // Non-critical section - multiple threads can execute
        String threadName = Thread.currentThread().getName();
        System.out.println(threadName + " preparing to update data...");
        
        // Critical section - only one thread can execute
        synchronized(lock) {
            System.out.println(threadName + " entered critical section");
            
            int oldValue = data;
            
            try {
                Thread.sleep(50); // Simulate processing
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }
            
            data = newValue;
            System.out.println(threadName + " updated data: " + 
                             oldValue + " -> " + data);
        }
        
        // Non-critical section continues
        System.out.println(threadName + " exited critical section");
    }
    
    // Alternative: synchronize on 'this'
    public void alternativeUpdate(int newValue) {
        synchronized(this) { // Equivalent to synchronized method
            data = newValue;
        }
    }
    
    public int getData() {
        synchronized(lock) {
            return data;
        }
    }
}

// Demonstration
public class SynchronizationDemo {
    public static void main(String[] args) {
        BankAccount account = new BankAccount("12345", 1000.0);
        
        // Create multiple threads accessing the same account
        Thread depositor1 = new Thread(() -> account.deposit(200.0), "Depositor-1");
        Thread depositor2 = new Thread(() -> account.deposit(300.0), "Depositor-2");
        Thread withdrawer = new Thread(() -> account.withdraw(150.0), "Withdrawer");
        
        depositor1.start();
        depositor2.start();
        withdrawer.start();
        
        try {
            depositor1.join();
            depositor2.join();
            withdrawer.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("Final balance: $" + account.getBalance());
    }
}
                

Producer-Consumer Problem

Classic Synchronization Challenge


import java.util.ArrayList;
import java.util.List;

class SharedBuffer {
    private final List buffer;
    private final int capacity;
    
    public SharedBuffer(int capacity) {
        this.capacity = capacity;
        this.buffer = new ArrayList<>();
    }
    
    // Producer method - adds items to buffer
    public synchronized void produce(int item) throws InterruptedException {
        // Wait while buffer is full
        while (buffer.size() == capacity) {
            System.out.println("Buffer full. Producer waiting...");
            wait(); // Release lock and wait
        }
        
        buffer.add(item);
        System.out.println("Produced: " + item + " | Buffer size: " + buffer.size());
        
        notify(); // Wake up waiting consumers
    }
    
    // Consumer method - removes items from buffer
    public synchronized int consume() throws InterruptedException {
        // Wait while buffer is empty
        while (buffer.isEmpty()) {
            System.out.println("Buffer empty. Consumer waiting...");
            wait(); // Release lock and wait
        }
        
        int item = buffer.remove(0);
        System.out.println("Consumed: " + item + " | Buffer size: " + buffer.size());
        
        notify(); // Wake up waiting producers
        return item;
    }
    
    public synchronized int size() {
        return buffer.size();
    }
}

class Producer implements Runnable {
    private final SharedBuffer buffer;
    private final String name;
    private final int itemCount;
    
    public Producer(SharedBuffer buffer, String name, int itemCount) {
        this.buffer = buffer;
        this.name = name;
        this.itemCount = itemCount;
    }
    
    @Override
    public void run() {
        try {
            for (int i = 1; i <= itemCount; i++) {
                int item = Integer.parseInt(name.substring(name.length()-1)) * 100 + i;
                buffer.produce(item);
                Thread.sleep(200); // Simulate production time
            }
        } catch (InterruptedException e) {
            System.out.println(name + " interrupted");
            Thread.currentThread().interrupt();
        }
        System.out.println(name + " finished producing");
    }
}
                

Consumer Implementation


class Consumer implements Runnable {
    private final SharedBuffer buffer;
    private final String name;
    private final int itemCount;
    
    public Consumer(SharedBuffer buffer, String name, int itemCount) {
        this.buffer = buffer;
        this.name = name;
        this.itemCount = itemCount;
    }
    
    @Override
    public void run() {
        try {
            for (int i = 1; i <= itemCount; i++) {
                int item = buffer.consume();
                System.out.println(name + " got item: " + item);
                Thread.sleep(300); // Simulate consumption time
            }
        } catch (InterruptedException e) {
            System.out.println(name + " interrupted");
            Thread.currentThread().interrupt();
        }
        System.out.println(name + " finished consuming");
    }
}

// Complete Producer-Consumer demonstration
public class ProducerConsumerDemo {
    public static void main(String[] args) {
        final int BUFFER_SIZE = 5;
        final int ITEMS_PER_THREAD = 3;
        
        SharedBuffer buffer = new SharedBuffer(BUFFER_SIZE);
        
        // Create producers and consumers
        Producer producer1 = new Producer(buffer, "Producer-1", ITEMS_PER_THREAD);
        Producer producer2 = new Producer(buffer, "Producer-2", ITEMS_PER_THREAD);
        
        Consumer consumer1 = new Consumer(buffer, "Consumer-1", ITEMS_PER_THREAD);
        Consumer consumer2 = new Consumer(buffer, "Consumer-2", ITEMS_PER_THREAD);
        
        // Create and start threads
        Thread p1Thread = new Thread(producer1);
        Thread p2Thread = new Thread(producer2);
        Thread c1Thread = new Thread(consumer1);
        Thread c2Thread = new Thread(consumer2);
        
        System.out.println("Starting Producer-Consumer simulation...");
        System.out.println("Buffer capacity: " + BUFFER_SIZE);
        
        // Start all threads
        p1Thread.start();
        p2Thread.start();
        c1Thread.start();
        c2Thread.start();
        
        // Wait for all threads to complete
        try {
            p1Thread.join();
            p2Thread.join();
            c1Thread.join();
            c2Thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("Producer-Consumer simulation completed");
        System.out.println("Final buffer size: " + buffer.size());
    }
}
                

GTU Previous Year Question (Summer 2022)

Q: Write a Java program to create two threads. One thread should display even numbers from 1 to 20 and another thread should display odd numbers from 1 to 20. Implement proper synchronization to ensure threads execute in an alternating manner.

Solution:


class NumberPrinter {
    private int currentNumber = 1;
    private final int maxNumber = 20;
    private boolean isEvenTurn = false; // false = odd turn, true = even turn
    
    // Method for printing odd numbers
    public synchronized void printOdd() {
        while (currentNumber <= maxNumber) {
            // Wait if it's not odd number's turn
            while (isEvenTurn && currentNumber <= maxNumber) {
                try {
                    wait(); // Wait for even thread to signal
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return;
                }
            }
            
            // Print odd number if within range
            if (currentNumber <= maxNumber && currentNumber % 2 == 1) {
                System.out.println("Odd Thread: " + currentNumber);
                currentNumber++;
                isEvenTurn = true; // Switch turn to even thread
                notify(); // Wake up even thread
            }
        }
    }
    
    // Method for printing even numbers
    public synchronized void printEven() {
        while (currentNumber <= maxNumber) {
            // Wait if it's not even number's turn
            while (!isEvenTurn && currentNumber <= maxNumber) {
                try {
                    wait(); // Wait for odd thread to signal
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return;
                }
            }
            
            // Print even number if within range
            if (currentNumber <= maxNumber && currentNumber % 2 == 0) {
                System.out.println("Even Thread: " + currentNumber);
                currentNumber++;
                isEvenTurn = false; // Switch turn to odd thread
                notify(); // Wake up odd thread
            }
        }
    }
}
                

Thread Classes and Main Method:


// Thread class for printing odd numbers
class OddNumberThread extends Thread {
    private NumberPrinter printer;
    
    public OddNumberThread(NumberPrinter printer) {
        this.printer = printer;
        this.setName("OddThread");
    }
    
    @Override
    public void run() {
        printer.printOdd();
    }
}

// Thread class for printing even numbers
class EvenNumberThread extends Thread {
    private NumberPrinter printer;
    
    public EvenNumberThread(NumberPrinter printer) {
        this.printer = printer;
        this.setName("EvenThread");
    }
    
    @Override
    public void run() {
        printer.printEven();
    }
}

// Main demonstration class
public class AlternatingNumbersDemo {
    public static void main(String[] args) {
        System.out.println("=== Alternating Odd-Even Numbers (1-20) ===");
        
        NumberPrinter printer = new NumberPrinter();
        
        // Create threads
        OddNumberThread oddThread = new OddNumberThread(printer);
        EvenNumberThread evenThread = new EvenNumberThread(printer);
        
        // Start threads
        System.out.println("Starting threads...\n");
        oddThread.start();  // Start with odd numbers
        evenThread.start();
        
        // Wait for both threads to complete
        try {
            oddThread.join();
            evenThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("\n=== All numbers printed successfully ===");
        System.out.println("Both threads completed execution");
    }
}
                
Key Features:
  • Perfect alternation between odd and even numbers
  • Synchronized access using wait() and notify()
  • Thread-safe number generation and printing
  • Clean termination when range is completed

GTU Previous Year Question (Winter 2022)

Q: Explain thread lifecycle in Java with a diagram. Write a program to demonstrate different thread states and transitions between them.

Thread Lifecycle Diagram:

Thread State Transitions:

NEW → start() → RUNNABLE → run() completes → TERMINATED

RUNNABLE → sleep()/wait() → TIMED_WAITING/WAITING → notify()/timeout → RUNNABLE

RUNNABLE → synchronized block → BLOCKED → lock acquired → RUNNABLE

Thread State Demonstration Program:


class StateMonitoringTask implements Runnable {
    private final Object lock = new Object();
    private volatile boolean shouldWait = true;
    
    @Override
    public void run() {
        Thread currentThread = Thread.currentThread();
        System.out.println(currentThread.getName() + " entered RUNNABLE state");
        
        try {
            // Demonstrate TIMED_WAITING state
            System.out.println(currentThread.getName() + " going to TIMED_WAITING (sleep)");
            Thread.sleep(2000);
            System.out.println(currentThread.getName() + " returned from TIMED_WAITING");
            
            // Demonstrate WAITING state
            synchronized (lock) {
                if (shouldWait) {
                    System.out.println(currentThread.getName() + " going to WAITING state");
                    lock.wait(); // WAITING state
                    System.out.println(currentThread.getName() + " returned from WAITING");
                }
            }
            
            // Demonstrate BLOCKED state by trying to acquire lock
            System.out.println(currentThread.getName() + " trying to acquire external lock");
            
            // Some additional work
            for (int i = 1; i <= 3; i++) {
                System.out.println(currentThread.getName() + " working... step " + i);
                Thread.sleep(500);
            }
            
        } catch (InterruptedException e) {
            System.out.println(currentThread.getName() + " was interrupted");
            Thread.currentThread().interrupt();
        }
        
        System.out.println(currentThread.getName() + " finished - going to TERMINATED");
    }
    
    public void releaseWaiting() {
        synchronized (lock) {
            shouldWait = false;
            lock.notify();
        }
    }
}
                

Thread State Monitor and Demo:


public class ThreadLifecycleDemo {
    public static void main(String[] args) {
        System.out.println("=== Thread Lifecycle Demonstration ===\n");
        
        StateMonitoringTask task = new StateMonitoringTask();
        Thread workerThread = new Thread(task, "WorkerThread");
        
        // Monitor thread states
        ThreadStateMonitor monitor = new ThreadStateMonitor(workerThread);
        Thread monitorThread = new Thread(monitor, "MonitorThread");
        
        System.out.println("1. Thread created - State: " + workerThread.getState());
        
        // Start monitoring and worker threads
        monitorThread.start();
        workerThread.start();
        
        try {
            // Let thread work for a while
            Thread.sleep(3000);
            
            // Release waiting thread
            System.out.println("Main thread releasing waiting worker thread");
            task.releaseWaiting();
            
            // Wait for threads to complete
            workerThread.join();
            monitor.stopMonitoring();
            monitorThread.join();
            
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("\nFinal state: " + workerThread.getState());
        System.out.println("Thread lifecycle demonstration completed");
    }
}

class ThreadStateMonitor implements Runnable {
    private final Thread threadToMonitor;
    private volatile boolean monitoring = true;
    private Thread.State lastState;
    
    public ThreadStateMonitor(Thread thread) {
        this.threadToMonitor = thread;
        this.lastState = thread.getState();
    }
    
    @Override
    public void run() {
        System.out.println("State monitor started");
        
        while (monitoring && threadToMonitor.getState() != Thread.State.TERMINATED) {
            Thread.State currentState = threadToMonitor.getState();
            
            if (currentState != lastState) {
                System.out.printf("[MONITOR] %s: %s → %s%n", 
                                threadToMonitor.getName(), lastState, currentState);
                lastState = currentState;
            }
            
            try {
                Thread.sleep(100); // Check every 100ms
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        
        System.out.println("State monitoring stopped");
    }
    
    public void stopMonitoring() {
        monitoring = false;
    }
}
                
States Demonstrated:
  • NEW: Thread created but not started
  • RUNNABLE: Thread executing or ready to execute
  • TIMED_WAITING: Thread.sleep() call
  • WAITING: Object.wait() call
  • TERMINATED: Thread completed execution

GTU Previous Year Question (Summer 2023)

Q: Write a Java program to implement producer-consumer problem using multithreading. The program should have bounded buffer with proper synchronization to avoid race conditions.

Complete Producer-Consumer Solution:


import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.Random;

// Product class representing items in the buffer
class Product {
    private final int id;
    private final String name;
    private final double price;
    
    public Product(int id, String name, double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }
    
    public int getId() { return id; }
    public String getName() { return name; }
    public double getPrice() { return price; }
    
    @Override
    public String toString() {
        return String.format("Product{id=%d, name='%s', price=%.2f}", id, name, price);
    }
}

// Producer class
class ProductProducer implements Runnable {
    private final BlockingQueue buffer;
    private final String producerName;
    private final int productCount;
    private final Random random = new Random();
    
    public ProductProducer(BlockingQueue buffer, String name, int count) {
        this.buffer = buffer;
        this.producerName = name;
        this.productCount = count;
    }
    
    @Override
    public void run() {
        try {
            for (int i = 1; i <= productCount; i++) {
                // Create a new product
                Product product = new Product(
                    random.nextInt(1000) + 1,
                    "Item-" + producerName + "-" + i,
                    10.0 + random.nextDouble() * 90.0
                );
                
                // Add to buffer (blocks if buffer is full)
                buffer.put(product);
                
                System.out.printf("[%s] Produced: %s | Buffer size: %d%n", 
                                producerName, product, buffer.size());
                
                // Simulate production time
                Thread.sleep(random.nextInt(500) + 100);
            }
        } catch (InterruptedException e) {
            System.out.println(producerName + " was interrupted");
            Thread.currentThread().interrupt();
        }
        
        System.out.println(producerName + " finished producing " + productCount + " products");
    }
}
                

Consumer Implementation:


// Consumer class
class ProductConsumer implements Runnable {
    private final BlockingQueue buffer;
    private final String consumerName;
    private final int productCount;
    private final Random random = new Random();
    private double totalValue = 0.0;
    
    public ProductConsumer(BlockingQueue buffer, String name, int count) {
        this.buffer = buffer;
        this.consumerName = name;
        this.productCount = count;
    }
    
    @Override
    public void run() {
        try {
            for (int i = 1; i <= productCount; i++) {
                // Take from buffer (blocks if buffer is empty)
                Product product = buffer.take();
                
                totalValue += product.getPrice();
                
                System.out.printf("[%s] Consumed: %s | Buffer size: %d%n", 
                                consumerName, product, buffer.size());
                
                // Simulate consumption time
                Thread.sleep(random.nextInt(700) + 200);
            }
        } catch (InterruptedException e) {
            System.out.println(consumerName + " was interrupted");
            Thread.currentThread().interrupt();
        }
        
        System.out.printf("%s finished consuming %d products | Total value: $%.2f%n", 
                         consumerName, productCount, totalValue);
    }
    
    public double getTotalValue() { return totalValue; }
}

// Main demonstration class
public class ProducerConsumerSolution {
    public static void main(String[] args) {
        final int BUFFER_SIZE = 10;
        final int PRODUCTS_PER_PRODUCER = 5;
        final int PRODUCTS_PER_CONSUMER = 5;
        
        // Create bounded buffer using BlockingQueue
        BlockingQueue buffer = new ArrayBlockingQueue<>(BUFFER_SIZE);
        
        System.out.println("=== Producer-Consumer Problem Solution ===");
        System.out.println("Buffer capacity: " + BUFFER_SIZE);
        System.out.println("Products per producer: " + PRODUCTS_PER_PRODUCER);
        System.out.println("Products per consumer: " + PRODUCTS_PER_CONSUMER);
        System.out.println();
        
        // Create producers and consumers
        ProductProducer producer1 = new ProductProducer(buffer, "Producer-A", PRODUCTS_PER_PRODUCER);
        ProductProducer producer2 = new ProductProducer(buffer, "Producer-B", PRODUCTS_PER_PRODUCER);
        
        ProductConsumer consumer1 = new ProductConsumer(buffer, "Consumer-X", PRODUCTS_PER_CONSUMER);
        ProductConsumer consumer2 = new ProductConsumer(buffer, "Consumer-Y", PRODUCTS_PER_CONSUMER);
        
        // Create and start threads
        Thread p1 = new Thread(producer1);
        Thread p2 = new Thread(producer2);
        Thread c1 = new Thread(consumer1);
        Thread c2 = new Thread(consumer2);
        
        long startTime = System.currentTimeMillis();
        
        // Start all threads
        p1.start();
        p2.start();
        c1.start();
        c2.start();
        
        // Wait for all threads to complete
        try {
            p1.join();
            p2.join();
            c1.join();
            c2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        long endTime = System.currentTimeMillis();
        
        System.out.println("\n=== Execution Summary ===");
        System.out.println("Total execution time: " + (endTime - startTime) + " ms");
        System.out.println("Final buffer size: " + buffer.size());
        System.out.println("Consumer-X total value: $" + String.format("%.2f", consumer1.getTotalValue()));
        System.out.println("Consumer-Y total value: $" + String.format("%.2f", consumer2.getTotalValue()));
        System.out.println("Producer-Consumer problem solved successfully!");
    }
}
                
Solution Features:
  • Bounded buffer with configurable capacity
  • Thread-safe operations using BlockingQueue
  • No race conditions or data corruption
  • Proper thread coordination and synchronization
  • Realistic simulation with variable timing

🧪 Hands-on Lab Exercise

Lab 15: Multi-threaded File Processor

Task: Create a multi-threaded file processing system that reads multiple files concurrently, processes their content, and writes results to output files.

Requirements:

  • Create a FileProcessor class that implements Runnable
  • Process multiple text files concurrently (word count, line count, character count)
  • Use synchronized collection to store results
  • Implement proper thread coordination with a summary thread
  • Handle file I/O exceptions properly
  • Display real-time progress from each thread
Challenge: Ensure thread-safe access to shared data structures and implement proper cleanup in case of exceptions.

📚 Lecture Summary

Key Concepts Covered

  • Multithreading fundamentals and benefits
  • Thread creation methods (extends Thread, implements Runnable)
  • Thread lifecycle and state transitions
  • Race conditions and synchronization
  • Synchronized methods and blocks
  • Producer-consumer problem solution

Important Methods

  • start() - Create new thread
  • run() - Thread execution logic
  • join() - Wait for thread completion
  • sleep() - Pause thread execution
  • wait() - Wait for notification
  • notify()/notifyAll() - Wake up threads

🎯 Next Lecture Preview

Lecture 16: Advanced Thread Concepts

  • Thread pools and ExecutorService
  • Concurrent collections (ConcurrentHashMap, etc.)
  • Deadlock prevention and detection
  • Atomic operations and volatile keyword