Learning Objectives

Multithreading Fundamentals

Understand threads, concurrency, and thread lifecycle

Thread Synchronization

Master synchronization and avoid race conditions

Concurrency Utilities

Learn Executor framework and modern concurrency tools

Thread Safety

Build thread-safe applications and avoid common pitfalls

Introduction to Multithreading

What is Multithreading?

Multithreading is the ability of a program to execute multiple threads concurrently, allowing it to perform multiple tasks simultaneously. This is particularly useful for improving performance and responsiveness in applications.

Threads vs C Threads

C

Manual thread management, POSIX threads (pthreads), manual synchronization

Java

Built-in thread support, automatic memory management, rich concurrency utilities

Benefits of Multithreading

Improved Performance

Better CPU utilization

Responsiveness

UI remains responsive during long operations

Resource Sharing

Threads can share memory and resources

Economy

Creating threads is cheaper than creating processes

Creating Threads

Method 1: Extending Thread Class

public class MyThread extends Thread {
    private String threadName;
    
    public MyThread(String name) {
        this.threadName = name;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(threadName + " executing: " + i);
            try {
                Thread.sleep(1000); // Sleep for 1 second
            } catch (InterruptedException e) {
                System.out.println(threadName + " interrupted");
                return;
            }
        }
        System.out.println(threadName + " finished");
    }
}

// Usage
MyThread thread1 = new MyThread("Thread-1");
MyThread thread2 = new MyThread("Thread-2");

thread1.start(); // Start the thread
thread2.start(); // Start another thread

Method 2: Implementing Runnable Interface

public class MyRunnable implements Runnable {
    private String threadName;
    
    public MyRunnable(String name) {
        this.threadName = name;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(threadName + " executing: " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println(threadName + " interrupted");
                return;
            }
        }
        System.out.println(threadName + " finished");
    }
}

// Usage
Thread thread1 = new Thread(new MyRunnable("Thread-1"));
Thread thread2 = new Thread(new MyRunnable("Thread-2"));

thread1.start();
thread2.start();

Method 3: Lambda Expressions (Java 8+)

// Using lambda expressions
Thread thread1 = new Thread(() -> {
    for (int i = 0; i < 5; i++) {
        System.out.println("Lambda Thread executing: " + i);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("Lambda Thread interrupted");
            return;
        }
    }
    System.out.println("Lambda Thread finished");
});

thread1.start();

Thread Synchronization

The Problem: Race Conditions

When multiple threads access shared data simultaneously, race conditions can occur.

public class Counter {
    private int count = 0;
    
    public void increment() {
        count++; // This is not atomic!
    }
    
    public int getCount() {
        return count;
    }
}

public class RaceConditionDemo {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        
        // Create multiple threads that increment the 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();
                }
            });
        }
        
        // Start all threads
        for (Thread thread : threads) {
            thread.start();
        }
        
        // Wait for all threads to complete
        for (Thread thread : threads) {
            thread.join();
        }
        
        // Expected: 10000, but might get different results due to race conditions
        System.out.println("Final count: " + counter.getCount());
    }
}

Solution 1: Synchronized Methods

public class SynchronizedCounter {
    private int count = 0;
    
    // Synchronized method - only one thread can execute this at a time
    public synchronized void increment() {
        count++;
    }
    
    public synchronized int getCount() {
        return count;
    }
    
    // Synchronized block - more granular control
    public void incrementBy(int amount) {
        synchronized (this) {
            count += amount;
        }
    }
}

Solution 2: Volatile Keyword

public class VolatileDemo {
    // Volatile ensures visibility across threads
    private volatile boolean running = true;
    
    public void stop() {
        running = false;
    }
    
    public void run() {
        while (running) {
            // Do some work
            System.out.println("Running...");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        System.out.println("Stopped");
    }
}

Modern Concurrency Utilities

Executor Framework

The Executor framework provides a higher-level replacement for working with threads directly.

import java.util.concurrent.*;

public class ExecutorDemo {
    public static void main(String[] args) {
        // Single thread executor
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        
        // Fixed thread pool
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        
        // Cached thread pool
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        
        // Submit tasks
        Future future1 = singleThreadExecutor.submit(() -> {
            Thread.sleep(1000);
            return "Task 1 completed";
        });
        
        Future future2 = fixedThreadPool.submit(() -> {
            Thread.sleep(2000);
            return "Task 2 completed";
        });
        
        // Get results
        try {
            System.out.println(future1.get()); // Waits for completion
            System.out.println(future2.get(5, TimeUnit.SECONDS)); // With timeout
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        // Shutdown executors
        singleThreadExecutor.shutdown();
        fixedThreadPool.shutdown();
        cachedThreadPool.shutdown();
    }
}

CompletableFuture (Java 8+)

CompletableFuture provides a way to write asynchronous, non-blocking code.

import java.util.concurrent.CompletableFuture;

public class CompletableFutureDemo {
    public static void main(String[] args) {
        // Simple async task
        CompletableFuture future1 = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            return "Hello";
        });
        
        // Chain operations
        CompletableFuture future2 = future1
            .thenApply(str -> str + " World")
            .thenApply(str -> str + "!")
            .thenApply(String::toUpperCase);
        
        // Handle completion
        future2.thenAccept(result -> System.out.println("Result: " + result));
        
        // Combine multiple futures
        CompletableFuture future3 = CompletableFuture.supplyAsync(() -> "Async");
        CompletableFuture future4 = CompletableFuture.supplyAsync(() -> "Task");
        
        CompletableFuture combined = future3.thenCombine(future4, (a, b) -> a + " " + b);
        combined.thenAccept(result -> System.out.println("Combined: " + result));
    }
}

Concurrent Collections

Thread-safe collections designed for concurrent access.

import java.util.concurrent.*;

public class ConcurrentCollectionsDemo {
    public static void main(String[] args) {
        // Thread-safe list
        List concurrentList = new CopyOnWriteArrayList<>();
        
        // Thread-safe map
        Map concurrentMap = new ConcurrentHashMap<>();
        
        // Thread-safe queue
        BlockingQueue blockingQueue = new LinkedBlockingQueue<>();
        
        // Thread-safe set
        Set concurrentSet = ConcurrentHashMap.newKeySet();
        
        // Add elements concurrently
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        // Add to concurrent list
        for (int i = 0; i < 3; i++) {
            final int threadId = i;
            executor.submit(() -> {
                for (int j = 0; j < 5; j++) {
                    concurrentList.add("Thread-" + threadId + "-Item-" + j);
                }
            });
        }
        
        executor.shutdown();
        
        try {
            executor.awaitTermination(5, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        System.out.println("Concurrent list size: " + concurrentList.size());
        System.out.println("Concurrent map size: " + concurrentMap.size());
    }
}

Practical Examples

Producer-Consumer with Blocking Queue

import java.util.concurrent.*;

public class ProducerConsumerWithBlockingQueue {
    private final BlockingQueue queue;
    private final int maxSize;
    
    public ProducerConsumerWithBlockingQueue(int maxSize) {
        this.queue = new ArrayBlockingQueue<>(maxSize);
        this.maxSize = maxSize;
    }
    
    public void start() {
        // Start producer
        Thread producer = new Thread(() -> {
            try {
                for (int i = 0; i < 20; i++) {
                    queue.put(i); // Blocks if queue is full
                    System.out.println("Produced: " + i);
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        
        // Start consumer
        Thread consumer = new Thread(() -> {
            try {
                for (int i = 0; i < 20; i++) {
                    int item = queue.take(); // Blocks if queue is empty
                    System.out.println("Consumed: " + item);
                    Thread.sleep(200);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        
        producer.start();
        consumer.start();
        
        try {
            producer.join();
            consumer.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

Key Differences from C

Concept C Java
Thread Creation pthread_create, manual management Built-in Thread class, automatic management
Synchronization Mutexes, semaphores, condition variables synchronized, wait/notify, concurrent utilities
Memory Management Manual allocation/deallocation Automatic garbage collection
Thread Safety Manual implementation Built-in thread-safe collections
Error Handling Return codes, errno Exception handling
Resource Management Manual cleanup Automatic with try-with-resources

Summary

In this module, you've learned:

  • ? How to create and manage threads in Java
  • ? Thread synchronization techniques and avoiding race conditions
  • ? Modern concurrency utilities like Executor framework and CompletableFuture
  • ? Thread-safe collections and atomic operations
  • ? Best practices for multithreaded programming
  • ? Key differences between Java threading and C threading approaches

Next Steps

  1. Complete the exercises in the exercises.md file
  2. Practice multithreaded programming and thread synchronization
  3. Move to Module 9: Java 8+ Features

?? Module 8 Complete!

You now have a solid understanding of multithreading and concurrency. Time to learn about modern Java features!