import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; import java.util.ArrayList; import java.util.List; /** * Comprehensive Multithreading Example in Java * * WHAT IS MULTITHREADING? * Multithreading allows your program to run multiple tasks simultaneously (concurrently). * Instead of doing one thing at a time, you can have multiple "threads" of execution * working on different tasks at the same time. * * WHY USE MULTITHREADING? * - Better performance: While one thread waits for I/O, others can continue working * - Responsive user interfaces: UI stays responsive while background tasks run * - Parallel processing: Utilize multiple CPU cores effectively * - Better resource utilization: Keep CPU busy instead of waiting * * KEY CONCEPTS COVERED: * - Thread creation and management (3 different ways) * - Synchronization mechanisms (preventing data corruption) * - Thread pools and executors (efficient thread management) * - Atomic operations (thread-safe operations without locks) * - Producer-Consumer pattern (classic synchronization pattern) * - CompletableFuture (modern asynchronous programming) * * IMPORTANT: Multithreading can be tricky! Always be careful with shared data. */ public class MultithreadingExample { // ===== SHARED RESOURCES FOR DEMONSTRATION ===== // These variables will be accessed by multiple threads simultaneously // This is where problems can occur if not handled properly! private static int sharedCounter = 0; // A simple counter that multiple threads will modify private static final Object lock = new Object(); // A lock object for synchronization // Note: ReentrantLock is demonstrated in the ThreadSafeCounter class below private static final AtomicInteger atomicCounter = new AtomicInteger(0); // Thread-safe counter // Producer-Consumer shared data // This list will be shared between producer and consumer threads private static final List sharedList = new ArrayList<>(); private static final int MAX_SIZE = 5; // Maximum items the list can hold public static void main(String[] args) { System.out.println("=== Java Multithreading Examples ===\n"); // Example 1: Basic Thread Creation basicThreadExample(); // Example 2: Thread Synchronization synchronizationExample(); // Example 3: Thread Pools threadPoolExample(); // Example 4: Atomic Operations atomicOperationsExample(); // Example 5: Producer-Consumer Pattern producerConsumerExample(); // Example 6: CompletableFuture completableFutureExample(); } /** * Example 1: Basic Thread Creation and Management * * WHAT IS A THREAD? * A thread is like a separate path of execution in your program. * Think of it as having multiple workers doing different tasks at the same time. * * THREE WAYS TO CREATE THREADS: * 1. Extending Thread class (old way, not recommended for new code) * 2. Implementing Runnable interface (recommended) * 3. Using lambda expressions (modern, clean way) * * WHY IMPLEMENT RUNNABLE INSTEAD OF EXTENDING THREAD? * - Java doesn't allow multiple inheritance, so extending Thread limits your options * - Runnable is more flexible - you can implement other interfaces too * - Better separation of concerns - the task is separate from the thread */ private static void basicThreadExample() { System.out.println("1. Basic Thread Creation:"); System.out.println(" (Watch how all three threads run at the same time!)\n"); // ===== METHOD 1: EXTENDING THREAD CLASS ===== // This is the old way - not recommended for new code // We create a new class that extends Thread and override the run() method Thread thread1 = new Thread() { @Override public void run() { // This code will run in a separate thread for (int i = 0; i < 5; i++) { System.out.println("Thread 1 - Count: " + i); try { Thread.sleep(100); // Sleep for 100ms (simulate work) } catch (InterruptedException e) { // If someone interrupts this thread, stop gracefully Thread.currentThread().interrupt(); break; } } } }; // ===== METHOD 2: IMPLEMENTING RUNNABLE INTERFACE ===== // This is the recommended way - more flexible // We pass a Runnable object to the Thread constructor Thread thread2 = new Thread(() -> { // This lambda expression implements the Runnable interface for (int i = 0; i < 5; i++) { System.out.println("Thread 2 - Count: " + i); try { Thread.sleep(150); // Sleep for 150ms } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } }); // ===== METHOD 3: USING LAMBDA EXPRESSION ===== // This is the modern, clean way to create threads // The lambda automatically implements Runnable Thread thread3 = new Thread(() -> { for (int i = 0; i < 5; i++) { System.out.println("Thread 3 - Count: " + i); try { Thread.sleep(200); // Sleep for 200ms } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } }); // ===== STARTING THREADS ===== // start() begins the thread execution // If you call run() instead of start(), it runs in the current thread (not what we want!) thread1.start(); thread2.start(); thread3.start(); // ===== WAITING FOR THREADS TO COMPLETE ===== // join() makes the current thread wait until the specified thread finishes // This ensures we don't exit the program before all threads are done try { thread1.join(); // Wait for thread1 to finish thread2.join(); // Wait for thread2 to finish thread3.join(); // Wait for thread3 to finish } catch (InterruptedException e) { // If the current thread is interrupted while waiting, handle it Thread.currentThread().interrupt(); } System.out.println("Basic thread example completed.\n"); } /** * Example 2: Thread Synchronization * * WHAT IS SYNCHRONIZATION? * When multiple threads access the same data at the same time, problems can occur. * Synchronization ensures that only one thread can access shared data at a time. * * THE PROBLEM WITHOUT SYNCHRONIZATION: * - Race conditions: Two threads might read the same value, modify it, and write back * - Data corruption: The final result might be wrong * - Unpredictable behavior: Results vary between runs * * THE SOLUTION: SYNCHRONIZED BLOCKS * - Only one thread can enter a synchronized block at a time * - Other threads must wait until the current thread exits * - This prevents race conditions and ensures data integrity * * WHY IS THIS IMPORTANT? * Imagine two people trying to update the same bank account balance at once! * Without synchronization, money could be lost or duplicated. */ private static void synchronizationExample() { System.out.println("2. Thread Synchronization:"); System.out.println(" (5 threads each incrementing a counter 1000 times)"); System.out.println(" (Expected result: 5000, but without sync it might be less!)\n"); // Reset shared counter to 0 sharedCounter = 0; // Create 5 threads that will all try to increment the same counter Thread[] threads = new Thread[5]; for (int i = 0; i < 5; i++) { final int threadId = i; // Need final for use in lambda threads[i] = new Thread(() -> { // Each thread will increment the counter 1000 times for (int j = 0; j < 1000; j++) { // ===== SYNCHRONIZED BLOCK ===== // This ensures only one thread can access sharedCounter at a time // The 'lock' object is used as a "key" - only one thread can hold it synchronized (lock) { sharedCounter++; // This operation is now thread-safe } // When this block ends, the lock is released for other threads } System.out.println("Thread " + threadId + " completed"); }); } // Start all 5 threads at the same time for (Thread thread : threads) { thread.start(); } // Wait for all threads to complete their work for (Thread thread : threads) { try { thread.join(); // Wait for this specific thread to finish } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } // Display the final result System.out.println("Final shared counter value: " + sharedCounter); System.out.println(" (Should be exactly 5000 if synchronization worked correctly)"); System.out.println("Synchronization example completed.\n"); } /** * Example 3: Thread Pools and Executors * * WHAT IS A THREAD POOL? * Instead of creating and destroying threads constantly, we create a "pool" of reusable threads. * This is much more efficient than creating new threads for each task. * * WHY USE THREAD POOLS? * - Performance: Creating/destroying threads is expensive * - Resource management: Limits the number of threads (prevents system overload) * - Reusability: Threads can be reused for multiple tasks * - Better control: Easy to manage and monitor thread usage * * TYPES OF THREAD POOLS: * - FixedThreadPool: Fixed number of threads * - CachedThreadPool: Creates threads as needed, reuses idle ones * - SingleThreadExecutor: Only one thread (tasks run sequentially) * - ScheduledThreadPool: For delayed or periodic tasks * * EXECUTORSERVICE: * - Manages the thread pool * - Submits tasks for execution * - Returns Future objects to get results * - Must be shut down when done! */ private static void threadPoolExample() { System.out.println("3. Thread Pools and Executors:"); System.out.println(" (10 tasks will be executed by 3 reusable threads)\n"); // ===== CREATE A FIXED THREAD POOL ===== // This creates a pool with exactly 3 threads // These threads will be reused for all our tasks ExecutorService executor = Executors.newFixedThreadPool(3); // ===== SUBMIT TASKS TO THE THREAD POOL ===== // We'll create 10 tasks but only 3 threads will handle them // Tasks will be queued and executed as threads become available List> futures = new ArrayList<>(); for (int i = 0; i < 10; i++) { final int taskId = i; // Need final for lambda // Submit a task to the thread pool // This returns a Future object that we can use to get the result later Future future = executor.submit(() -> { try { // Simulate some work (1 second) Thread.sleep(1000); // Return a result with the task ID and thread name return "Task " + taskId + " completed by " + Thread.currentThread().getName(); } catch (InterruptedException e) { // Handle interruption gracefully Thread.currentThread().interrupt(); return "Task " + taskId + " interrupted"; } }); futures.add(future); // Store the Future object for later } // ===== COLLECT RESULTS ===== // Now we'll get the results from all our tasks // The get() method blocks until the task completes for (Future future : futures) { try { System.out.println(future.get()); // Get the result (this might wait) } catch (InterruptedException | ExecutionException e) { System.err.println("Error getting result: " + e.getMessage()); } } // ===== SHUTDOWN THE EXECUTOR ===== // IMPORTANT: Always shut down the executor when done! // This prevents the program from hanging executor.shutdown(); try { // Wait up to 60 seconds for all tasks to complete if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { // If tasks don't finish in time, force shutdown executor.shutdownNow(); } } catch (InterruptedException e) { // If interrupted while waiting, force shutdown executor.shutdownNow(); Thread.currentThread().interrupt(); } System.out.println("Thread pool example completed.\n"); } /** * Example 4: Atomic Operations * * WHAT ARE ATOMIC OPERATIONS? * Atomic operations are operations that complete in a single step without interruption. * They are thread-safe by design - no synchronization needed! * * WHY USE ATOMIC OPERATIONS? * - Thread-safe: No need for synchronized blocks * - Performance: Often faster than synchronization * - Simplicity: Less code, fewer bugs * - Lock-free: No risk of deadlocks * * COMMON ATOMIC TYPES: * - AtomicInteger: Thread-safe integer * - AtomicLong: Thread-safe long * - AtomicBoolean: Thread-safe boolean * - AtomicReference: Thread-safe object reference * * ATOMIC METHODS: * - get(): Get current value * - set(newValue): Set new value * - incrementAndGet(): Increment and return new value * - compareAndSet(expected, new): Set only if current value equals expected */ private static void atomicOperationsExample() { System.out.println("4. Atomic Operations:"); System.out.println(" (5 threads incrementing atomic counter 1000 times each)"); System.out.println(" (No synchronization needed - atomic operations are thread-safe!)\n"); // Reset atomic counter to 0 atomicCounter.set(0); // Create 5 threads that will increment the atomic counter Thread[] threads = new Thread[5]; for (int i = 0; i < 5; i++) { final int threadId = i; threads[i] = new Thread(() -> { // Each thread increments the counter 1000 times for (int j = 0; j < 1000; j++) { // ===== ATOMIC OPERATION ===== // This operation is thread-safe by design! // No synchronized block needed - the operation is atomic atomicCounter.incrementAndGet(); } System.out.println("Atomic Thread " + threadId + " completed"); }); } // 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) { Thread.currentThread().interrupt(); } } // Display the final result System.out.println("Final atomic counter value: " + atomicCounter.get()); System.out.println(" (Should be exactly 5000 - atomic operations guarantee correctness)"); System.out.println("Atomic operations example completed.\n"); } /** * Example 5: Producer-Consumer Pattern * * WHAT IS THE PRODUCER-CONSUMER PATTERN? * A classic synchronization pattern where: * - Producer threads create data and put it in a shared buffer * - Consumer threads take data from the buffer and process it * - The buffer has a limited size * * THE CHALLENGE: * - Producer must wait when buffer is full * - Consumer must wait when buffer is empty * - Both must coordinate to avoid race conditions * * THE SOLUTION: WAIT/NOTIFY MECHANISM * - wait(): Makes a thread wait until another thread calls notify() * - notify(): Wakes up one waiting thread * - notifyAll(): Wakes up all waiting threads * - Always use wait() in a while loop, not if statement! * * REAL-WORLD EXAMPLES: * - Message queues * - Task queues * - Data processing pipelines * - Event handling systems */ private static void producerConsumerExample() { System.out.println("5. Producer-Consumer Pattern:"); System.out.println(" (Producer creates items, Consumer processes them)"); System.out.println(" (Buffer can hold max " + MAX_SIZE + " items)\n"); // Clear the shared list (our buffer) sharedList.clear(); // ===== PRODUCER THREAD ===== // Creates items and puts them in the buffer Thread producer = new Thread(() -> { for (int i = 1; i <= 10; i++) { synchronized (sharedList) { // ===== WAIT WHILE BUFFER IS FULL ===== // Use while loop, not if statement! // This handles spurious wakeups and multiple producers while (sharedList.size() >= MAX_SIZE) { try { System.out.println("Producer waiting - buffer full"); sharedList.wait(); // Wait until consumer removes an item } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } // Buffer has space, add the item sharedList.add(i); System.out.println("Produced: " + i + " (buffer size: " + sharedList.size() + ")"); sharedList.notifyAll(); // Wake up any waiting consumers } // Simulate some work between productions try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } System.out.println("Producer finished"); }); // ===== CONSUMER THREAD ===== // Takes items from the buffer and processes them Thread consumer = new Thread(() -> { for (int i = 0; i < 10; i++) { synchronized (sharedList) { // ===== WAIT WHILE BUFFER IS EMPTY ===== while (sharedList.isEmpty()) { try { System.out.println("Consumer waiting - buffer empty"); sharedList.wait(); // Wait until producer adds an item } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } // Buffer has items, take one int value = sharedList.remove(0); System.out.println("Consumed: " + value + " (buffer size: " + sharedList.size() + ")"); sharedList.notifyAll(); // Wake up any waiting producers } // Simulate some work between consumptions try { Thread.sleep(150); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } System.out.println("Consumer finished"); }); // Start both threads producer.start(); consumer.start(); // Wait for both threads to complete try { producer.join(); consumer.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Producer-Consumer example completed.\n"); } /** * Example 6: CompletableFuture */ private static void completableFutureExample() { System.out.println("6. CompletableFuture:"); // Create a CompletableFuture CompletableFuture future = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(1000); return "Hello from CompletableFuture!"; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return "Interrupted"; } }); // Chain operations CompletableFuture chainedFuture = future .thenApply(result -> result + " - Processed") .thenApply(String::toUpperCase) .thenCompose(result -> CompletableFuture.supplyAsync(() -> result + " - Composed")); // Handle multiple futures CompletableFuture future1 = CompletableFuture.supplyAsync(() -> "Future 1"); CompletableFuture future2 = CompletableFuture.supplyAsync(() -> "Future 2"); CompletableFuture combinedFuture = future1.thenCombine(future2, (result1, result2) -> result1 + " + " + result2); // Get results try { System.out.println("Chained result: " + chainedFuture.get()); System.out.println("Combined result: " + combinedFuture.get()); } catch (InterruptedException | ExecutionException e) { System.err.println("Error getting CompletableFuture result: " + e.getMessage()); } System.out.println("CompletableFuture example completed.\n"); } /** * Example of a custom thread-safe counter using ReentrantLock */ static class ThreadSafeCounter { private int count = 0; private final ReentrantLock lock = new ReentrantLock(); public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } public int getCount() { lock.lock(); try { return count; } finally { lock.unlock(); } } } /** * Example of a custom thread that can be interrupted gracefully */ static class InterruptibleThread extends Thread { @Override public void run() { while (!Thread.currentThread().isInterrupted()) { try { // Do some work System.out.println("Working..."); Thread.sleep(1000); } catch (InterruptedException e) { System.out.println("Thread interrupted, cleaning up..."); Thread.currentThread().interrupt(); break; } } System.out.println("Thread finished gracefully"); } } }