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

2 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 16: Advanced Thread Concepts | Java Programming (4343203)

Advanced Thread Concepts

Java Programming (4343203)

Lecture 16

Unit 4: Advanced Java - Concurrency
GTU Computer Engineering Semester 4

Learning Objectives

  • Master ExecutorService and thread pools
  • Understand concurrent collections and atomic operations
  • Learn deadlock prevention and detection techniques
  • Implement thread-safe patterns and best practices
  • Use volatile keyword and memory consistency
  • Apply Fork-Join framework for parallel processing
Focus: Professional-grade concurrent programming techniques for scalable applications.

Thread Pools and ExecutorService

Why Thread Pools?

  • Avoid thread creation overhead
  • Control resource consumption
  • Improve application performance
  • Better thread lifecycle management

ExecutorService Types

  • newFixedThreadPool(n)
  • newCachedThreadPool()
  • newSingleThreadExecutor()
  • newScheduledThreadPool(n)

Basic ExecutorService Usage


import java.util.concurrent.*;

ExecutorService executor = 
    Executors.newFixedThreadPool(4);

// Submit tasks
for (int i = 1; i <= 10; i++) {
    final int taskId = i;
    executor.submit(() -> {
        System.out.println("Task " + taskId + 
                          " on " + Thread.currentThread().getName());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });
}

// Shutdown executor
executor.shutdown();
try {
    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
        executor.shutdownNow();
    }
} catch (InterruptedException e) {
    executor.shutdownNow();
}
                        

Comprehensive ExecutorService Example


import java.util.concurrent.*;
import java.util.List;
import java.util.ArrayList;
import java.util.Random;

// Task class for demonstration
class DataProcessingTask implements Callable {
    private final int taskId;
    private final int[] data;
    private final Random random = new Random();
    
    public DataProcessingTask(int taskId, int[] data) {
        this.taskId = taskId;
        this.data = data;
    }
    
    @Override
    public ProcessingResult call() throws Exception {
        String threadName = Thread.currentThread().getName();
        System.out.println("Task " + taskId + " started on " + threadName);
        
        // Simulate complex processing
        Thread.sleep(random.nextInt(2000) + 1000);
        
        // Calculate sum, average, max, min
        long sum = 0;
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        
        for (int value : data) {
            sum += value;
            max = Math.max(max, value);
            min = Math.min(min, value);
        }
        
        double average = (double) sum / data.length;
        
        System.out.println("Task " + taskId + " completed on " + threadName);
        return new ProcessingResult(taskId, sum, average, max, min);
    }
}

// Result class
class ProcessingResult {
    private final int taskId;
    private final long sum;
    private final double average;
    private final int max, min;
    
    public ProcessingResult(int taskId, long sum, double average, int max, int min) {
        this.taskId = taskId;
        this.sum = sum;
        this.average = average;
        this.max = max;
        this.min = min;
    }
    
    @Override
    public String toString() {
        return String.format("Task %d: Sum=%d, Avg=%.2f, Max=%d, Min=%d", 
                           taskId, sum, average, max, min);
    }
}
                

ExecutorService Demonstration


public class ExecutorServiceDemo {
    public static void main(String[] args) {
        final int TASK_COUNT = 6;
        final int DATA_SIZE = 1000;
        
        // Create thread pool
        ExecutorService executor = Executors.newFixedThreadPool(3);
        List> futures = new ArrayList<>();
        
        System.out.println("=== ExecutorService Demo ===");
        System.out.println("Thread pool size: 3");
        System.out.println("Number of tasks: " + TASK_COUNT);
        
        long startTime = System.currentTimeMillis();
        
        // Submit tasks
        for (int i = 1; i <= TASK_COUNT; i++) {
            // Generate random data for each task
            int[] data = generateRandomData(DATA_SIZE);
            DataProcessingTask task = new DataProcessingTask(i, data);
            Future future = executor.submit(task);
            futures.add(future);
        }
        
        System.out.println("All tasks submitted to executor");
        
        // Collect results
        List results = new ArrayList<>();
        for (Future future : futures) {
            try {
                ProcessingResult result = future.get(); // Blocking call
                results.add(result);
            } catch (InterruptedException | ExecutionException e) {
                System.err.println("Task failed: " + e.getMessage());
            }
        }
        
        long endTime = System.currentTimeMillis();
        
        // Display results
        System.out.println("\n=== Results ===");
        results.forEach(System.out::println);
        
        System.out.printf("Total execution time: %d ms%n", endTime - startTime);
        
        // Shutdown executor
        executor.shutdown();
        try {
            if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
                executor.shutdownNow();
                System.out.println("Executor forcefully shutdown");
            } else {
                System.out.println("Executor shutdown gracefully");
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
        }
    }
    
    private static int[] generateRandomData(int size) {
        Random random = new Random();
        int[] data = new int[size];
        for (int i = 0; i < size; i++) {
            data[i] = random.nextInt(100) + 1;
        }
        return data;
    }
}
                

Atomic Operations and Variables

Why Atomic Operations?

  • Thread-safe without synchronization
  • Lock-free programming
  • Better performance than synchronized
  • Avoid race conditions

Common Atomic Classes

  • AtomicInteger
  • AtomicLong
  • AtomicBoolean
  • AtomicReference

import java.util.concurrent.atomic.*;

// Atomic counter example
class AtomicCounter {
    private final AtomicInteger count = 
        new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet();
    }
    
    public void decrement() {
        count.decrementAndGet();
    }
    
    public int get() {
        return count.get();
    }
    
    // Compare and swap operation
    public boolean compareAndSet(int expected, 
                               int newValue) {
        return count.compareAndSet(expected, 
                                 newValue);
    }
    
    // Atomic add and return
    public int addAndGet(int delta) {
        return count.addAndGet(delta);
    }
}
                        

Atomic Operations Demonstration


import java.util.concurrent.atomic.*;
import java.util.concurrent.*;

public class AtomicOperationsDemo {
    private static final int THREAD_COUNT = 5;
    private static final int OPERATIONS_PER_THREAD = 10000;
    
    // Compare atomic vs non-atomic counters
    private static volatile int regularCounter = 0;
    private static AtomicInteger atomicCounter = new AtomicInteger(0);
    
    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== Atomic Operations Demo ===");
        System.out.println("Threads: " + THREAD_COUNT);
        System.out.println("Operations per thread: " + OPERATIONS_PER_THREAD);
        
        // Test regular counter (race condition)
        testRegularCounter();
        
        // Test atomic counter (thread-safe)
        testAtomicCounter();
        
        // Demonstrate other atomic operations
        demonstrateAtomicOperations();
    }
    
    private static void testRegularCounter() throws InterruptedException {
        System.out.println("\n--- Testing Regular Counter (Race Condition) ---");
        regularCounter = 0;
        
        ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
        CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
        
        for (int i = 0; i < THREAD_COUNT; i++) {
            executor.submit(() -> {
                for (int j = 0; j < OPERATIONS_PER_THREAD; j++) {
                    regularCounter++; // Not thread-safe!
                }
                latch.countDown();
            });
        }
        
        latch.await();
        executor.shutdown();
        
        int expected = THREAD_COUNT * OPERATIONS_PER_THREAD;
        System.out.println("Expected: " + expected);
        System.out.println("Actual: " + regularCounter);
        System.out.println("Data corruption: " + (expected != regularCounter));
    }
    
    private static void testAtomicCounter() throws InterruptedException {
        System.out.println("\n--- Testing Atomic Counter (Thread-Safe) ---");
        atomicCounter.set(0);
        
        ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
        CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
        
        for (int i = 0; i < THREAD_COUNT; i++) {
            executor.submit(() -> {
                for (int j = 0; j < OPERATIONS_PER_THREAD; j++) {
                    atomicCounter.incrementAndGet(); // Thread-safe!
                }
                latch.countDown();
            });
        }
        
        latch.await();
        executor.shutdown();
        
        int expected = THREAD_COUNT * OPERATIONS_PER_THREAD;
        System.out.println("Expected: " + expected);
        System.out.println("Actual: " + atomicCounter.get());
        System.out.println("Thread-safe: " + (expected == atomicCounter.get()));
    }
}
                

Volatile Keyword and Memory Consistency

What is Volatile?

  • Ensures visibility across threads
  • Prevents instruction reordering
  • Not atomic for compound operations
  • Lighter than synchronization
Important: Volatile doesn't guarantee atomicity for operations like i++

class VolatileExample {
    private volatile boolean shutdown = false;
    private volatile int counter = 0;
    
    // Worker thread
    public void worker() {
        while (!shutdown) {
            // Do work
            System.out.println("Working... " + 
                             counter);
            counter++; // Not atomic!
            
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        System.out.println("Worker shutdown");
    }
    
    // Main thread
    public void stopWorker() {
        shutdown = true; // Visible immediately
    }
    
    public boolean isShutdown() {
        return shutdown;
    }
}
                        

Concurrent Collections

Thread-Safe Collection Classes


import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class ConcurrentCollectionsDemo {
    public static void main(String[] args) throws InterruptedException {
        // 1. ConcurrentHashMap - Thread-safe HashMap
        ConcurrentHashMap concurrentMap = new ConcurrentHashMap<>();
        
        // 2. CopyOnWriteArrayList - Thread-safe ArrayList (for read-heavy scenarios)
        CopyOnWriteArrayList safeList = new CopyOnWriteArrayList<>();
        
        // 3. BlockingQueue - Thread-safe queue with blocking operations
        BlockingQueue queue = new ArrayBlockingQueue<>(100);
        
        // 4. ConcurrentLinkedQueue - Non-blocking thread-safe queue
        ConcurrentLinkedQueue linkedQueue = new ConcurrentLinkedQueue<>();
        
        System.out.println("=== Concurrent Collections Demo ===");
        
        // Demonstrate ConcurrentHashMap
        demonstrateConcurrentHashMap(concurrentMap);
        
        // Demonstrate BlockingQueue
        demonstrateBlockingQueue(queue);
    }
    
    private static void demonstrateConcurrentHashMap(
            ConcurrentHashMap map) throws InterruptedException {
        
        System.out.println("\n--- ConcurrentHashMap Demo ---");
        
        ExecutorService executor = Executors.newFixedThreadPool(4);
        AtomicInteger totalOperations = new AtomicInteger(0);
        
        // Multiple threads updating the same map
        for (int i = 1; i <= 4; i++) {
            final int threadId = i;
            executor.submit(() -> {
                for (int j = 1; j <= 10; j++) {
                    String key = "key" + (j % 5); // 5 different keys
                    
                    // Atomic increment using compute method
                    map.compute(key, (k, v) -> {
                        totalOperations.incrementAndGet();
                        return (v == null) ? 1 : v + 1;
                    });
                    
                    System.out.printf("Thread-%d: Updated %s = %d%n", 
                                    threadId, key, map.get(key));
                }
            });
        }
        
        executor.shutdown();
        executor.awaitTermination(10, TimeUnit.SECONDS);
        
        System.out.println("Final map contents: " + map);
        System.out.println("Total operations: " + totalOperations.get());
    }
}
                

Deadlock Prevention and Detection

Deadlock Conditions

  1. Mutual Exclusion: Resources cannot be shared
  2. Hold and Wait: Thread holds resources while waiting
  3. No Preemption: Resources cannot be forcibly taken
  4. Circular Wait: Circular dependency of resource requests

Prevention Strategies

  • Ordered lock acquisition
  • Timeout-based locking
  • Avoid nested locks
  • Use higher-level concurrency utilities

Deadlock Example


// BAD: Can cause deadlock
class DeadlockRisk {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    
    public void method1() {
        synchronized(lock1) {
            synchronized(lock2) {
                // Critical section
            }
        }
    }
    
    public void method2() {
        synchronized(lock2) {
            synchronized(lock1) { // Deadlock!
                // Critical section
            }
        }
    }
}
                            

Deadlock Prevention Example


import java.util.concurrent.locks.*;

// GOOD: Deadlock prevention using ordered locking
class BankAccount {
    private final int accountId;
    private double balance;
    private final ReentrantLock lock = new ReentrantLock();
    
    public BankAccount(int id, double initialBalance) {
        this.accountId = id;
        this.balance = initialBalance;
    }
    
    // Deadlock-free money transfer using ordered locking
    public static boolean transfer(BankAccount from, BankAccount to, double amount) {
        if (from == to) return false; // Same account
        
        // Order locks by account ID to prevent deadlock
        BankAccount firstLock = from.accountId < to.accountId ? from : to;
        BankAccount secondLock = from.accountId < to.accountId ? to : from;
        
        firstLock.lock.lock();
        try {
            secondLock.lock.lock();
            try {
                // Check sufficient funds
                if (from.balance >= amount) {
                    from.balance -= amount;
                    to.balance += amount;
                    
                    System.out.printf("Transferred $%.2f: Account-%d -> Account-%d%n",
                                    amount, from.accountId, to.accountId);
                    System.out.printf("Balances: Account-%d=$%.2f, Account-%d=$%.2f%n",
                                    from.accountId, from.balance, to.accountId, to.balance);
                    return true;
                } else {
                    System.out.printf("Transfer failed: Insufficient funds in Account-%d%n", 
                                    from.accountId);
                    return false;
                }
            } finally {
                secondLock.lock.unlock();
            }
        } finally {
            firstLock.lock.unlock();
        }
    }
    
    // Alternative: Using tryLock with timeout
    public static boolean transferWithTimeout(BankAccount from, BankAccount to, 
                                            double amount, long timeoutMs) {
        try {
            if (from.lock.tryLock(timeoutMs, TimeUnit.MILLISECONDS)) {
                try {
                    if (to.lock.tryLock(timeoutMs, TimeUnit.MILLISECONDS)) {
                        try {
                            if (from.balance >= amount) {
                                from.balance -= amount;
                                to.balance += amount;
                                return true;
                            }
                        } finally {
                            to.lock.unlock();
                        }
                    }
                } finally {
                    from.lock.unlock();
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return false; // Transfer failed or timed out
    }
    
    public double getBalance() {
        lock.lock();
        try {
            return balance;
        } finally {
            lock.unlock();
        }
    }
}
                

GTU Previous Year Question (Summer 2022)

Q: Write a Java program using ExecutorService to process multiple tasks concurrently. Each task should perform mathematical calculations (factorial, prime check, etc.) and return results. Demonstrate proper thread pool management and result collection.

Solution:


import java.util.concurrent.*;
import java.util.List;
import java.util.ArrayList;
import java.math.BigInteger;

// Task for mathematical calculations
class MathTask implements Callable {
    private final int number;
    private final String operation;
    private final int taskId;
    
    public MathTask(int taskId, int number, String operation) {
        this.taskId = taskId;
        this.number = number;
        this.operation = operation;
    }
    
    @Override
    public MathResult call() throws Exception {
        String threadName = Thread.currentThread().getName();
        System.out.printf("Task %d (%s for %d) started on %s%n", 
                         taskId, operation, number, threadName);
        
        long startTime = System.currentTimeMillis();
        Object result;
        
        switch (operation.toLowerCase()) {
            case "factorial":
                result = calculateFactorial(number);
                break;
            case "prime":
                result = isPrime(number);
                break;
            case "fibonacci":
                result = calculateFibonacci(number);
                break;
            case "sum":
                result = calculateSum(number);
                break;
            default:
                throw new IllegalArgumentException("Unknown operation: " + operation);
        }
        
        long executionTime = System.currentTimeMillis() - startTime;
        
        System.out.printf("Task %d completed on %s in %d ms%n", 
                         taskId, threadName, executionTime);
        
        return new MathResult(taskId, number, operation, result, executionTime, threadName);
    }
    
    private BigInteger calculateFactorial(int n) throws InterruptedException {
        if (n < 0) throw new IllegalArgumentException("Factorial not defined for negative numbers");
        if (n > 50) throw new IllegalArgumentException("Number too large for factorial calculation");
        
        BigInteger result = BigInteger.ONE;
        for (int i = 2; i <= n; i++) {
            result = result.multiply(BigInteger.valueOf(i));
            // Simulate work and check for interruption
            if (i % 5 == 0) {
                Thread.sleep(10);
                if (Thread.currentThread().isInterrupted()) {
                    throw new InterruptedException("Factorial calculation interrupted");
                }
            }
        }
        return result;
    }
    
    private boolean isPrime(int n) throws InterruptedException {
        if (n < 2) return false;
        if (n == 2) return true;
        if (n % 2 == 0) return false;
        
        for (int i = 3; i * i <= n; i += 2) {
            if (n % i == 0) return false;
            // Check for interruption periodically
            if (i % 1000 == 3) {
                Thread.sleep(1);
                if (Thread.currentThread().isInterrupted()) {
                    throw new InterruptedException("Prime check interrupted");
                }
            }
        }
        return true;
    }
}
                

Additional Methods and Result Class:


    private long calculateFibonacci(int n) throws InterruptedException {
        if (n < 0) throw new IllegalArgumentException("Fibonacci not defined for negative numbers");
        if (n > 50) throw new IllegalArgumentException("Number too large for Fibonacci calculation");
        
        if (n <= 1) return n;
        
        long a = 0, b = 1, result = 0;
        for (int i = 2; i <= n; i++) {
            result = a + b;
            a = b;
            b = result;
            
            if (i % 5 == 0) {
                Thread.sleep(5);
                if (Thread.currentThread().isInterrupted()) {
                    throw new InterruptedException("Fibonacci calculation interrupted");
                }
            }
        }
        return result;
    }
    
    private long calculateSum(int n) throws InterruptedException {
        if (n < 1) throw new IllegalArgumentException("Sum calculation requires positive number");
        
        long sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
            if (i % 10000 == 0) {
                Thread.sleep(1);
                if (Thread.currentThread().isInterrupted()) {
                    throw new InterruptedException("Sum calculation interrupted");
                }
            }
        }
        return sum;
    }

// Result class to hold calculation results
class MathResult {
    private final int taskId;
    private final int inputNumber;
    private final String operation;
    private final Object result;
    private final long executionTime;
    private final String threadName;
    
    public MathResult(int taskId, int inputNumber, String operation, 
                     Object result, long executionTime, String threadName) {
        this.taskId = taskId;
        this.inputNumber = inputNumber;
        this.operation = operation;
        this.result = result;
        this.executionTime = executionTime;
        this.threadName = threadName;
    }
    
    @Override
    public String toString() {
        return String.format("Task %d: %s(%d) = %s [%d ms on %s]", 
                           taskId, operation, inputNumber, result, executionTime, threadName);
    }
    
    // Getters
    public int getTaskId() { return taskId; }
    public String getOperation() { return operation; }
    public long getExecutionTime() { return executionTime; }
    public Object getResult() { return result; }
}
                

Main Demonstration Class:


public class ConcurrentMathProcessor {
    public static void main(String[] args) {
        final int THREAD_POOL_SIZE = 4;
        
        System.out.println("=== Concurrent Mathematical Task Processor ===");
        System.out.println("Thread pool size: " + THREAD_POOL_SIZE);
        
        // Create thread pool
        ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
        List> futures = new ArrayList<>();
        
        // Create various mathematical tasks
        Object[][] tasks = {
            {1, 10, "factorial"},     // 10!
            {2, 97, "prime"},         // Check if 97 is prime
            {3, 20, "fibonacci"},     // 20th Fibonacci number
            {4, 1000, "sum"},         // Sum of 1 to 1000
            {5, 15, "factorial"},     // 15!
            {6, 101, "prime"},        // Check if 101 is prime
            {7, 25, "fibonacci"},     // 25th Fibonacci number
            {8, 5000, "sum"},         // Sum of 1 to 5000
            {9, 89, "prime"},         // Check if 89 is prime
            {10, 12, "factorial"}     // 12!
        };
        
        long startTime = System.currentTimeMillis();
        
        // Submit all tasks
        for (Object[] taskData : tasks) {
            int taskId = (Integer) taskData[0];
            int number = (Integer) taskData[1];
            String operation = (String) taskData[2];
            
            MathTask task = new MathTask(taskId, number, operation);
            Future future = executor.submit(task);
            futures.add(future);
        }
        
        System.out.println("All tasks submitted to executor\n");
        
        // Collect results
        List results = new ArrayList<>();
        int completedTasks = 0;
        
        for (Future future : futures) {
            try {
                MathResult result = future.get(10, TimeUnit.SECONDS); // Timeout after 10 seconds
                results.add(result);
                completedTasks++;
            } catch (InterruptedException e) {
                System.err.println("Task interrupted: " + e.getMessage());
                Thread.currentThread().interrupt();
            } catch (ExecutionException e) {
                System.err.println("Task execution failed: " + e.getCause().getMessage());
            } catch (TimeoutException e) {
                System.err.println("Task timed out");
                future.cancel(true); // Interrupt the task
            }
        }
        
        long totalTime = System.currentTimeMillis() - startTime;
        
        // Display results
        System.out.println("\n=== EXECUTION RESULTS ===");
        results.stream()
               .sorted((r1, r2) -> Integer.compare(r1.getTaskId(), r2.getTaskId()))
               .forEach(System.out::println);
        
        // Display statistics
        System.out.println("\n=== EXECUTION STATISTICS ===");
        System.out.println("Total tasks: " + tasks.length);
        System.out.println("Completed tasks: " + completedTasks);
        System.out.println("Failed tasks: " + (tasks.length - completedTasks));
        System.out.println("Total execution time: " + totalTime + " ms");
        
        long totalTaskTime = results.stream().mapToLong(MathResult::getExecutionTime).sum();
        System.out.println("Sum of individual task times: " + totalTaskTime + " ms");
        System.out.println("Speedup factor: " + String.format("%.2fx", (double)totalTaskTime / totalTime));
        
        // Shutdown executor
        shutdownExecutor(executor);
    }
}
                

Proper ExecutorService Shutdown:


    private static void shutdownExecutor(ExecutorService executor) {
        System.out.println("\n=== SHUTDOWN PROCESS ===");
        
        // Disable new tasks from being submitted
        executor.shutdown();
        
        try {
            // Wait a while for existing tasks to terminate
            if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
                System.out.println("Executor did not terminate gracefully, forcing shutdown...");
                
                // Cancel currently executing tasks
                executor.shutdownNow();
                
                // Wait a while for tasks to respond to being cancelled
                if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
                    System.err.println("Executor did not terminate after forced shutdown");
                } else {
                    System.out.println("Executor terminated after forced shutdown");
                }
            } else {
                System.out.println("Executor terminated gracefully");
            }
        } catch (InterruptedException e) {
            System.out.println("Shutdown interrupted, forcing immediate shutdown...");
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
}
                
Solution Features:
  • Multiple mathematical operations (factorial, prime, Fibonacci, sum)
  • Proper ExecutorService lifecycle management
  • Result collection using Future objects
  • Error handling and timeout management
  • Performance statistics and speedup calculation
  • Graceful shutdown with fallback to forced termination

GTU Previous Year Question (Winter 2022)

Q: Explain the concept of deadlock in multithreading. Write a Java program that demonstrates both deadlock situation and its prevention using proper synchronization techniques.

Deadlock Demonstration and Prevention:


import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;

// Resource class for demonstration
class Resource {
    private final int id;
    private final String name;
    private final ReentrantLock lock = new ReentrantLock();
    
    public Resource(int id, String name) {
        this.id = id;
        this.name = name;
    }
    
    public void acquire(String threadName) {
        System.out.printf("%s trying to acquire %s%n", threadName, name);
        lock.lock();
        System.out.printf("%s acquired %s%n", threadName, name);
    }
    
    public boolean tryAcquire(String threadName, long timeoutMs) {
        System.out.printf("%s trying to acquire %s (with timeout)%n", threadName, name);
        try {
            if (lock.tryLock(timeoutMs, TimeUnit.MILLISECONDS)) {
                System.out.printf("%s acquired %s%n", threadName, name);
                return true;
            } else {
                System.out.printf("%s failed to acquire %s (timeout)%n", threadName, name);
                return false;
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
    
    public void release(String threadName) {
        lock.unlock();
        System.out.printf("%s released %s%n", threadName, name);
    }
    
    public int getId() { return id; }
    public String getName() { return name; }
}

// Task that can cause deadlock
class DeadlockTask implements Runnable {
    private final Resource resource1;
    private final Resource resource2;
    private final String taskName;
    
    public DeadlockTask(String name, Resource r1, Resource r2) {
        this.taskName = name;
        this.resource1 = r1;
        this.resource2 = r2;
    }
    
    @Override
    public void run() {
        try {
            // Acquire first resource
            resource1.acquire(taskName);
            
            // Simulate some work
            Thread.sleep(100);
            
            // Try to acquire second resource - potential deadlock here!
            resource2.acquire(taskName);
            
            System.out.printf("%s: Successfully acquired both resources!%n", taskName);
            
            // Simulate critical section work
            Thread.sleep(200);
            
        } catch (InterruptedException e) {
            System.out.printf("%s interrupted%n", taskName);
            Thread.currentThread().interrupt();
        } finally {
            // Release resources in reverse order
            try {
                resource2.release(taskName);
            } catch (Exception e) {
                // Resource might not have been acquired
            }
            resource1.release(taskName);
        }
    }
}
                

Deadlock Prevention Implementation:


// Task with deadlock prevention using ordered locking
class DeadlockFreeTask implements Runnable {
    private final Resource resource1;
    private final Resource resource2;
    private final String taskName;
    
    public DeadlockFreeTask(String name, Resource r1, Resource r2) {
        this.taskName = name;
        this.resource1 = r1;
        this.resource2 = r2;
    }
    
    @Override
    public void run() {
        // Order resources by ID to prevent circular wait
        Resource firstResource = resource1.getId() < resource2.getId() ? resource1 : resource2;
        Resource secondResource = resource1.getId() < resource2.getId() ? resource2 : resource1;
        
        try {
            // Acquire resources in ordered manner
            firstResource.acquire(taskName);
            
            Thread.sleep(50); // Simulate work
            
            secondResource.acquire(taskName);
            
            System.out.printf("%s: Successfully acquired both resources (deadlock-free)!%n", taskName);
            
            // Critical section work
            Thread.sleep(100);
            
        } catch (InterruptedException e) {
            System.out.printf("%s interrupted%n", taskName);
            Thread.currentThread().interrupt();
        } finally {
            // Release in reverse order
            try {
                secondResource.release(taskName);
            } catch (Exception e) {
                // Might not have been acquired
            }
            firstResource.release(taskName);
        }
    }
}

// Task with timeout-based deadlock prevention
class TimeoutBasedTask implements Runnable {
    private final Resource resource1;
    private final Resource resource2;
    private final String taskName;
    private final long timeoutMs;
    
    public TimeoutBasedTask(String name, Resource r1, Resource r2, long timeout) {
        this.taskName = name;
        this.resource1 = r1;
        this.resource2 = r2;
        this.timeoutMs = timeout;
    }
    
    @Override
    public void run() {
        boolean acquired1 = false, acquired2 = false;
        
        try {
            // Try to acquire first resource with timeout
            acquired1 = resource1.tryAcquire(taskName, timeoutMs);
            if (!acquired1) {
                System.out.printf("%s: Failed to acquire first resource%n", taskName);
                return;
            }
            
            Thread.sleep(50); // Simulate work
            
            // Try to acquire second resource with timeout
            acquired2 = resource2.tryAcquire(taskName, timeoutMs);
            if (!acquired2) {
                System.out.printf("%s: Failed to acquire second resource%n", taskName);
                return;
            }
            
            System.out.printf("%s: Successfully acquired both resources (timeout-based)!%n", taskName);
            
            // Critical section work
            Thread.sleep(100);
            
        } catch (InterruptedException e) {
            System.out.printf("%s interrupted%n", taskName);
            Thread.currentThread().interrupt();
        } finally {
            // Release acquired resources
            if (acquired2) resource2.release(taskName);
            if (acquired1) resource1.release(taskName);
        }
    }
}
                

Complete Deadlock Demonstration:


public class DeadlockDemo {
    public static void main(String[] args) {
        Resource resourceA = new Resource(1, "Resource-A");
        Resource resourceB = new Resource(2, "Resource-B");
        
        System.out.println("=== DEADLOCK DEMONSTRATION ===\n");
        
        // 1. Demonstrate deadlock scenario
        System.out.println("--- 1. Demonstrating Deadlock Scenario ---");
        demonstrateDeadlock(resourceA, resourceB);
        
        // Wait a bit between demonstrations
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        // 2. Demonstrate deadlock prevention using ordered locking
        System.out.println("\n--- 2. Deadlock Prevention: Ordered Locking ---");
        demonstrateOrderedLocking(resourceA, resourceB);
        
        // 3. Demonstrate deadlock prevention using timeouts
        System.out.println("\n--- 3. Deadlock Prevention: Timeout-Based ---");
        demonstrateTimeoutBasedPrevention(resourceA, resourceB);
    }
    
    private static void demonstrateDeadlock(Resource resourceA, Resource resourceB) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        
        // Task 1: A -> B
        DeadlockTask task1 = new DeadlockTask("Task-1", resourceA, resourceB);
        // Task 2: B -> A (creates circular dependency)
        DeadlockTask task2 = new DeadlockTask("Task-2", resourceB, resourceA);
        
        Future future1 = executor.submit(task1);
        Future future2 = executor.submit(task2);
        
        try {
            // Wait for tasks with timeout to detect deadlock
            future1.get(3, TimeUnit.SECONDS);
            future2.get(3, TimeUnit.SECONDS);
            System.out.println("Both tasks completed successfully (no deadlock)");
        } catch (TimeoutException e) {
            System.err.println("DEADLOCK DETECTED! Tasks timed out.");
            future1.cancel(true);
            future2.cancel(true);
        } catch (InterruptedException | ExecutionException e) {
            System.err.println("Task execution error: " + e.getMessage());
        }
        
        executor.shutdownNow();
    }
    
    private static void demonstrateOrderedLocking(Resource resourceA, Resource resourceB) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        
        DeadlockFreeTask task1 = new DeadlockFreeTask("OrderedTask-1", resourceA, resourceB);
        DeadlockFreeTask task2 = new DeadlockFreeTask("OrderedTask-2", resourceB, resourceA);
        
        Future future1 = executor.submit(task1);
        Future future2 = executor.submit(task2);
        
        try {
            future1.get(5, TimeUnit.SECONDS);
            future2.get(5, TimeUnit.SECONDS);
            System.out.println("Both ordered tasks completed successfully!");
        } catch (Exception e) {
            System.err.println("Ordered task execution error: " + e.getMessage());
        }
        
        executor.shutdown();
    }
    
    private static void demonstrateTimeoutBasedPrevention(Resource resourceA, Resource resourceB) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        
        TimeoutBasedTask task1 = new TimeoutBasedTask("TimeoutTask-1", resourceA, resourceB, 1000);
        TimeoutBasedTask task2 = new TimeoutBasedTask("TimeoutTask-2", resourceB, resourceA, 1000);
        
        Future future1 = executor.submit(task1);
        Future future2 = executor.submit(task2);
        
        try {
            future1.get(5, TimeUnit.SECONDS);
            future2.get(5, TimeUnit.SECONDS);
            System.out.println("Both timeout-based tasks completed!");
        } catch (Exception e) {
            System.err.println("Timeout-based task execution error: " + e.getMessage());
        }
        
        executor.shutdown();
    }
}
                
Deadlock Prevention Techniques Demonstrated:
  • Ordered Locking: Always acquire locks in the same order
  • Timeout-Based: Use tryLock() with timeout to avoid indefinite waiting
  • Detection: Monitor task execution time to detect potential deadlocks

🧪 Hands-on Lab Exercise

Lab 16: Concurrent Web Crawler

Task: Create a multi-threaded web scraper that processes URLs concurrently using ExecutorService, atomic counters, and concurrent collections.

Requirements:

  • Implement a WebCrawlerTask using Callable interface
  • Use ConcurrentHashMap to store crawled URLs and results
  • Implement atomic counters for success/failure statistics
  • Use CompletionService to process results as they complete
  • Implement proper timeout handling and resource cleanup
  • Create a monitoring thread to display real-time progress
Challenge: Implement rate limiting to avoid overwhelming target servers and handle various exception scenarios gracefully.

📚 Lecture Summary

Advanced Concepts Covered

  • ExecutorService and thread pools
  • Concurrent collections (ConcurrentHashMap, etc.)
  • Atomic operations and variables
  • Volatile keyword and memory visibility
  • Deadlock detection and prevention
  • Lock-free programming techniques

Best Practices

  • Use thread pools instead of creating threads manually
  • Prefer concurrent collections over synchronized ones
  • Use atomic variables for simple thread-safe operations
  • Implement proper resource cleanup and shutdown
  • Use ordered locking to prevent deadlocks
  • Handle interruptions and timeouts gracefully

🎯 Next Lecture Preview

Lecture 17: Java Collections Framework

  • List, Set, and Map interfaces
  • ArrayList, LinkedList, HashMap implementations
  • Iterators and enhanced for loops
  • Comparable and Comparator interfaces