1. java
  2. /concurrency
  3. /threads

Java Threads and Concurrent Programming

Java Threads: Mastering Concurrent Programming

Threads represent one of the most fundamental yet challenging aspects of modern Java development. As applications have evolved from simple, sequential programs to complex, interactive systems that must handle multiple users, process real-time data, and maintain responsiveness under load, understanding concurrent programming has become essential for every Java developer.

The Evolution from Sequential to Concurrent Programming

In the early days of computing, programs executed sequentially—one instruction after another in a predictable order. This model worked well for simple batch processing systems, but as user expectations grew and applications became more complex, developers needed ways to handle multiple tasks simultaneously.

Why Traditional Sequential Programming Falls Short:

User Experience: A single-threaded application that makes a database call or processes a large file would freeze the entire user interface, creating an unacceptable user experience.

Resource Utilization: Modern computers have multiple CPU cores, but sequential programs can only use one core at a time, leaving significant processing power unused.

I/O Bottlenecks: Applications often wait for disk reads, network calls, or database queries. During these waiting periods, the CPU sits idle instead of processing other tasks.

Scalability Limits: Web applications serving multiple users simultaneously cannot efficiently handle concurrent requests with a single-threaded approach.

Threads represent one of the most fundamental yet challenging aspects of modern Java development. Understanding threads is essential for building responsive, high-performance applications that can handle multiple tasks simultaneously. Java provides robust built-in support for multithreading through the Thread class and related concurrency utilities.

What are Threads?

A thread is an independent execution path within a program that allows multiple sequences of instructions to run concurrently within the same application. To understand threads, it's helpful to first understand the relationship between processes and threads in modern operating systems.

Processes vs. Threads: Understanding the Hierarchy

Processes: Think of a process as a complete program running on your operating system. Each Java application you start creates a new process. Processes are isolated from each other—they have separate memory spaces and cannot directly access each other's data.

Threads: Within each process, you can have multiple threads. These are like mini-programs that share the same memory space but can execute different tasks simultaneously. All threads within a process can access the same variables, objects, and resources.

Real-World Analogy: Imagine a restaurant (process) with multiple chefs (threads) working in the same kitchen. They share the same ingredients and equipment (memory), but each chef can prepare different dishes (tasks) simultaneously. If one chef waits for the oven to heat up, the others can continue chopping vegetables or preparing sauces.

Thread Fundamentals: How Concurrency Works

Concurrent vs. Parallel Execution:

  • Concurrency: Multiple threads making progress by taking turns using the CPU (time-slicing)
  • Parallelism: Multiple threads actually executing simultaneously on different CPU cores

Thread Lifecycle: Every thread goes through distinct states during its lifetime:

  • NEW: Thread created but not yet started
  • RUNNABLE: Thread is executing or ready to execute
  • BLOCKED: Thread waiting for a monitor lock
  • WAITING: Thread waiting indefinitely for another thread
  • TIMED_WAITING: Thread waiting for a specified period
  • TERMINATED: Thread has completed execution

Key Thread Characteristics:

1. Shared Memory Space: All threads within a process access the same heap memory, making data sharing efficient but requiring careful synchronization.

2. Independent Execution Stacks: Each thread has its own call stack, local variables, and program counter, allowing independent execution paths.

3. Lightweight Creation: Creating threads requires less system overhead than creating new processes.

4. Communication Mechanisms: Threads can communicate through shared objects, wait/notify mechanisms, and concurrent data structures.

5. Resource Sharing: Threads share file handles, network connections, and other system resources owned by the process.

Why Threads Matter in Modern Applications:

Responsiveness: Keep user interfaces responsive while performing background operations like file downloads or data processing.

Performance: Utilize multiple CPU cores effectively by distributing work across threads, leading to significant performance improvements on multi-core systems.

I/O Efficiency: While one thread waits for disk reads or network responses, other threads can continue processing, maximizing overall application throughput.

Modular Design: Separate different concerns (UI updates, data processing, network communication) into independent threads for cleaner architecture.

Scalability: Handle multiple concurrent users or requests efficiently, essential for server applications and web services.

Demonstrating the Power of Concurrent Execution

Let's examine a practical example that illustrates the dramatic difference between sequential and concurrent execution:

public class ThreadBasics {
    
    // Without threads - sequential execution
    public static void sequentialExample() {
        System.out.println("=== Sequential Execution ===");
        long startTime = System.currentTimeMillis();
        
        // Task 1: Simulate heavy computation
        heavyComputation("Task 1");
        
        // Task 2: Simulate another heavy computation
        heavyComputation("Task 2");
        
        long endTime = System.currentTimeMillis();
        System.out.println("Total time: " + (endTime - startTime) + "ms\n");
    }
    
    // With threads - concurrent execution
    public static void concurrentExample() throws InterruptedException {
        System.out.println("=== Concurrent Execution ===");
        long startTime = System.currentTimeMillis();
        
        // Create threads for parallel execution
        Thread thread1 = new Thread(() -> heavyComputation("Task 1"));
        Thread thread2 = new Thread(() -> heavyComputation("Task 2"));
        
        // Start both threads
        thread1.start();
        thread2.start();
        
        // Wait for both to complete
        thread1.join();
        thread2.join();
        
        long endTime = System.currentTimeMillis();
        System.out.println("Total time: " + (endTime - startTime) + "ms\n");
    }
    
    private static void heavyComputation(String taskName) {
        System.out.println(taskName + " started by " + Thread.currentThread().getName());
        
        // Simulate heavy work
        try {
            Thread.sleep(2000); // 2 seconds
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        System.out.println(taskName + " completed by " + Thread.currentThread().getName());
    }
    
    public static void main(String[] args) throws InterruptedException {
        sequentialExample();
        concurrentExample();
    }
}

Understanding the Performance Impact:

Sequential Execution Analysis:

  • Task 1 runs first and takes 2 seconds to complete
  • Only after Task 1 finishes does Task 2 start
  • Task 2 takes another 2 seconds to complete
  • Total execution time: approximately 4 seconds
  • Only one CPU core is utilized at a time

Concurrent Execution Analysis:

  • Both Task 1 and Task 2 start simultaneously
  • Each task still takes 2 seconds, but they run in parallel
  • Total execution time: approximately 2 seconds (50% reduction!)
  • Multiple CPU cores can be utilized simultaneously

Key Threading Concepts Demonstrated:

Thread Creation: new Thread(() -> heavyComputation("Task 1")) creates a new thread with a lambda expression defining the task to execute.

Thread Starting: thread1.start() actually begins thread execution. Never call run() directly—this would execute the task in the current thread, defeating the purpose.

Thread Joining: thread1.join() makes the main thread wait until thread1 completes. Without this, the main thread would finish before the worker threads, and the timing measurement would be inaccurate.

Thread Names: Each thread has a unique name (like "Thread-0", "Thread-1") that helps identify which thread is executing which task.

Real-World Applications: This pattern applies to scenarios like:

  • Processing multiple files simultaneously
  • Making concurrent API calls
  • Handling multiple user requests in web applications
  • Performing parallel calculations on large datasets

Creating Threads

Java provides several ways to create and start threads:

1. Extending Thread Class

public class ExtendingThreadExample {
    
    // Method 1: Extend Thread class
    static class MyThread extends Thread {
        private final String taskName;
        
        public MyThread(String taskName) {
            this.taskName = taskName;
        }
        
        @Override
        public void run() {
            for (int i = 1; i <= 5; i++) {
                System.out.println(taskName + " - Step " + i + " [" + 
                                 Thread.currentThread().getName() + "]");
                try {
                    Thread.sleep(1000); // Pause for 1 second
                } catch (InterruptedException e) {
                    System.out.println(taskName + " was interrupted");
                    return;
                }
            }
            System.out.println(taskName + " completed!");
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== Extending Thread Class ===");
        
        // Create thread instances
        MyThread thread1 = new MyThread("Download Task");
        MyThread thread2 = new MyThread("Upload Task");
        
        // Start threads
        thread1.start();
        thread2.start();
        
        // Wait for completion
        thread1.join();
        thread2.join();
        
        System.out.println("All tasks completed!");
    }
}

2. Implementing Runnable Interface

public class RunnableInterfaceExample {
    
    // Method 2: Implement Runnable interface (preferred)
    static class MyTask implements Runnable {
        private final String taskName;
        private final int iterations;
        
        public MyTask(String taskName, int iterations) {
            this.taskName = taskName;
            this.iterations = iterations;
        }
        
        @Override
        public void run() {
            for (int i = 1; i <= iterations; i++) {
                System.out.println(taskName + " - Iteration " + i + " [" + 
                                 Thread.currentThread().getName() + "]");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    System.out.println(taskName + " was interrupted");
                    Thread.currentThread().interrupt();
                    return;
                }
            }
            System.out.println(taskName + " finished!");
        }
    }
    
    // Using lambda expressions for simple tasks
    public static void demonstrateLambdaThreads() throws InterruptedException {
        System.out.println("\n=== Using Lambda Expressions ===");
        
        // Simple lambda thread
        Thread lambdaThread = new Thread(() -> {
            String threadName = Thread.currentThread().getName();
            for (int i = 1; i <= 3; i++) {
                System.out.println("Lambda task - Step " + i + " [" + threadName + "]");
                try {
                    Thread.sleep(800);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return;
                }
            }
        });
        
        lambdaThread.setName("LambdaWorker");
        lambdaThread.start();
        lambdaThread.join();
    }
    
    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== Implementing Runnable Interface ===");
        
        // Create runnable tasks
        MyTask task1 = new MyTask("Data Processing", 4);
        MyTask task2 = new MyTask("File Backup", 3);
        
        // Create threads with runnable tasks
        Thread thread1 = new Thread(task1);
        Thread thread2 = new Thread(task2);
        
        // Set thread names
        thread1.setName("ProcessorThread");
        thread2.setName("BackupThread");
        
        // Start threads
        thread1.start();
        thread2.start();
        
        // Wait for completion
        thread1.join();
        thread2.join();
        
        demonstrateLambdaThreads();
        
        System.out.println("All tasks completed!");
    }
}

3. Using Callable Interface

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

public class CallableExample {
    
    // Callable allows returning values and throwing checked exceptions
    static class CalculationTask implements Callable<Integer> {
        private final int start;
        private final int end;
        
        public CalculationTask(int start, int end) {
            this.start = start;
            this.end = end;
        }
        
        @Override
        public Integer call() throws Exception {
            String threadName = Thread.currentThread().getName();
            System.out.println("Calculating sum from " + start + " to " + end + 
                             " [" + threadName + "]");
            
            int sum = 0;
            for (int i = start; i <= end; i++) {
                sum += i;
                Thread.sleep(10); // Simulate work
            }
            
            System.out.println("Calculation completed [" + threadName + "]");
            return sum;
        }
    }
    
    public static void main(String[] args) {
        System.out.println("=== Using Callable Interface ===");
        
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        try {
            // Create callable tasks
            List<Callable<Integer>> tasks = Arrays.asList(
                new CalculationTask(1, 100),
                new CalculationTask(101, 200),
                new CalculationTask(201, 300)
            );
            
            // Submit tasks and get futures
            List<Future<Integer>> futures = executor.invokeAll(tasks);
            
            // Collect results
            int totalSum = 0;
            for (int i = 0; i < futures.size(); i++) {
                try {
                    Integer result = futures.get(i).get();
                    System.out.println("Task " + (i + 1) + " result: " + result);
                    totalSum += result;
                } catch (ExecutionException e) {
                    System.err.println("Task " + (i + 1) + " failed: " + e.getCause());
                }
            }
            
            System.out.println("Total sum: " + totalSum);
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("Tasks were interrupted");
        } finally {
            executor.shutdown();
        }
    }
}

Thread Lifecycle and States

Understanding thread states is crucial for effective thread management:

public class ThreadLifecycleExample {
    
    static class LifecycleThread extends Thread {
        private final Object lock = new Object();
        private volatile boolean shouldWait = false;
        
        @Override
        public void run() {
            System.out.println("Thread entered RUNNABLE state");
            
            try {
                // Simulate some work
                Thread.sleep(1000);
                System.out.println("Thread woke up from TIMED_WAITING");
                
                // Demonstrate WAITING state
                synchronized (lock) {
                    shouldWait = true;
                    System.out.println("Thread entering WAITING state");
                    lock.wait(); // Thread enters WAITING state
                    System.out.println("Thread resumed from WAITING state");
                }
                
                // More work
                Thread.sleep(500);
                System.out.println("Thread finishing execution");
                
            } catch (InterruptedException e) {
                System.out.println("Thread was interrupted");
                Thread.currentThread().interrupt();
            }
        }
        
        public void wakeUp() {
            synchronized (lock) {
                lock.notify();
            }
        }
        
        public boolean isWaiting() {
            return shouldWait && getState() == State.WAITING;
        }
    }
    
    public static void demonstrateThreadStates() throws InterruptedException {
        System.out.println("=== Thread Lifecycle Demonstration ===");
        
        LifecycleThread thread = new LifecycleThread();
        
        // NEW state
        System.out.println("1. Thread state: " + thread.getState()); // NEW
        
        // Start thread - moves to RUNNABLE
        thread.start();
        Thread.sleep(100); // Give thread time to start
        System.out.println("2. Thread state: " + thread.getState()); // RUNNABLE
        
        // Wait for thread to enter TIMED_WAITING
        Thread.sleep(600);
        System.out.println("3. Thread state: " + thread.getState()); // TIMED_WAITING
        
        // Wait for thread to enter WAITING state
        while (!thread.isWaiting()) {
            Thread.sleep(100);
        }
        System.out.println("4. Thread state: " + thread.getState()); // WAITING
        
        // Wake up the waiting thread
        Thread.sleep(1000);
        thread.wakeUp();
        
        // Wait for thread to complete
        thread.join();
        System.out.println("5. Thread state: " + thread.getState()); // TERMINATED
        
        System.out.println("Thread lifecycle demonstration completed");
    }
    
    public static void demonstrateBlockedState() throws InterruptedException {
        System.out.println("\n=== BLOCKED State Demonstration ===");
        
        Object sharedLock = new Object();
        
        Thread thread1 = new Thread(() -> {
            synchronized (sharedLock) {
                System.out.println("Thread1 acquired lock");
                try {
                    Thread.sleep(3000); // Hold lock for 3 seconds
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                System.out.println("Thread1 releasing lock");
            }
        }, "Thread1");
        
        Thread thread2 = new Thread(() -> {
            System.out.println("Thread2 attempting to acquire lock");
            synchronized (sharedLock) {
                System.out.println("Thread2 acquired lock");
            }
        }, "Thread2");
        
        thread1.start();
        Thread.sleep(100); // Let thread1 acquire lock first
        
        thread2.start();
        Thread.sleep(100); // Let thread2 attempt to acquire lock
        
        System.out.println("Thread2 state: " + thread2.getState()); // BLOCKED
        
        thread1.join();
        thread2.join();
        
        System.out.println("BLOCKED state demonstration completed");
    }
    
    public static void main(String[] args) throws InterruptedException {
        demonstrateThreadStates();
        demonstrateBlockedState();
    }
}

Thread Properties and Control

Threads have various properties that can be configured and controlled:

public class ThreadPropertiesExample {
    
    static class WorkerThread extends Thread {
        private final String taskName;
        
        public WorkerThread(String taskName) {
            this.taskName = taskName;
        }
        
        @Override
        public void run() {
            Thread currentThread = Thread.currentThread();
            System.out.println("\n=== Thread Properties ===");
            System.out.println("Task: " + taskName);
            System.out.println("Thread Name: " + currentThread.getName());
            System.out.println("Thread ID: " + currentThread.getId());
            System.out.println("Priority: " + currentThread.getPriority());
            System.out.println("Is Daemon: " + currentThread.isDaemon());
            System.out.println("Thread Group: " + currentThread.getThreadGroup().getName());
            System.out.println("Is Alive: " + currentThread.isAlive());
            
            // Simulate work
            for (int i = 1; i <= 3; i++) {
                System.out.println(taskName + " - Working... " + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println(taskName + " was interrupted");
                    return;
                }
            }
            
            System.out.println(taskName + " completed!");
        }
    }
    
    static class DaemonThread extends Thread {
        @Override
        public void run() {
            int count = 0;
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    count++;
                    System.out.println("Daemon thread running... " + count);
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                System.out.println("Daemon thread interrupted");
            }
            System.out.println("Daemon thread ending");
        }
    }
    
    public static void demonstrateThreadProperties() throws InterruptedException {
        System.out.println("=== Thread Properties Demonstration ===");
        
        // Create worker threads with different properties
        WorkerThread worker1 = new WorkerThread("HighPriorityTask");
        WorkerThread worker2 = new WorkerThread("LowPriorityTask");
        
        // Set thread names
        worker1.setName("HighPriorityWorker");
        worker2.setName("LowPriorityWorker");
        
        // Set thread priorities
        worker1.setPriority(Thread.MAX_PRIORITY);  // 10
        worker2.setPriority(Thread.MIN_PRIORITY);  // 1
        
        // Start threads
        worker1.start();
        worker2.start();
        
        // Wait for completion
        worker1.join();
        worker2.join();
    }
    
    public static void demonstrateDaemonThreads() throws InterruptedException {
        System.out.println("\n=== Daemon Thread Demonstration ===");
        
        DaemonThread daemonThread = new DaemonThread();
        daemonThread.setName("BackgroundDaemon");
        daemonThread.setDaemon(true); // Mark as daemon thread
        
        System.out.println("Starting daemon thread...");
        daemonThread.start();
        
        // Main thread does some work
        for (int i = 1; i <= 3; i++) {
            System.out.println("Main thread working... " + i);
            Thread.sleep(1500);
        }
        
        System.out.println("Main thread ending - daemon will automatically terminate");
        // JVM will exit and daemon thread will be terminated automatically
    }
    
    public static void demonstrateThreadInterruption() throws InterruptedException {
        System.out.println("\n=== Thread Interruption Demonstration ===");
        
        Thread longRunningTask = new Thread(() -> {
            try {
                for (int i = 1; i <= 10; i++) {
                    System.out.println("Long running task - iteration " + i);
                    Thread.sleep(1000);
                    
                    // Check for interruption
                    if (Thread.currentThread().isInterrupted()) {
                        System.out.println("Task detected interruption signal");
                        return;
                    }
                }
            } catch (InterruptedException e) {
                System.out.println("Task was interrupted during sleep");
                Thread.currentThread().interrupt(); // Restore interrupt status
            }
        });
        
        longRunningTask.setName("LongRunningTask");
        longRunningTask.start();
        
        // Let it run for a bit
        Thread.sleep(3500);
        
        // Interrupt the thread
        System.out.println("Interrupting long running task...");
        longRunningTask.interrupt();
        
        // Wait for it to finish
        longRunningTask.join();
        System.out.println("Thread interruption demonstration completed");
    }
    
    public static void main(String[] args) throws InterruptedException {
        demonstrateThreadProperties();
        demonstrateDaemonThreads();
        demonstrateThreadInterruption();
    }
}

Thread Communication and Coordination

Threads often need to communicate and coordinate their activities:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Semaphore;

public class ThreadCommunicationExample {
    
    // Producer-Consumer pattern using wait/notify
    static class ProducerConsumerExample {
        private final Object lock = new Object();
        private int data = 0;
        private boolean dataReady = false;
        
        class Producer extends Thread {
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    synchronized (lock) {
                        data = i * 10;
                        dataReady = true;
                        System.out.println("Producer: Data produced = " + data);
                        lock.notify(); // Wake up waiting consumer
                    }
                    
                    try {
                        Thread.sleep(1000); // Simulate production time
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            }
        }
        
        class Consumer extends Thread {
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    synchronized (lock) {
                        while (!dataReady) {
                            try {
                                System.out.println("Consumer: Waiting for data...");
                                lock.wait(); // Wait for data to be ready
                            } catch (InterruptedException e) {
                                Thread.currentThread().interrupt();
                                return;
                            }
                        }
                        
                        System.out.println("Consumer: Data consumed = " + data);
                        dataReady = false; // Mark data as consumed
                    }
                }
            }
        }
        
        public void demonstrate() throws InterruptedException {
            System.out.println("=== Producer-Consumer Example ===");
            
            Producer producer = new Producer();
            Consumer consumer = new Consumer();
            
            consumer.start();
            producer.start();
            
            producer.join();
            consumer.join();
            
            System.out.println("Producer-Consumer demonstration completed\n");
        }
    }
    
    // CountDownLatch example
    static class CountDownLatchExample {
        public void demonstrate() throws InterruptedException {
            System.out.println("=== CountDownLatch Example ===");
            
            int numWorkers = 3;
            CountDownLatch startSignal = new CountDownLatch(1);
            CountDownLatch doneSignal = new CountDownLatch(numWorkers);
            
            // Create worker threads
            for (int i = 1; i <= numWorkers; i++) {
                new Thread(new Worker(i, startSignal, doneSignal)).start();
            }
            
            System.out.println("All workers created, starting work...");
            startSignal.countDown(); // Release all workers
            
            doneSignal.await(); // Wait for all workers to complete
            System.out.println("All workers completed!\n");
        }
        
        class Worker implements Runnable {
            private final int workerId;
            private final CountDownLatch startSignal;
            private final CountDownLatch doneSignal;
            
            Worker(int workerId, CountDownLatch startSignal, CountDownLatch doneSignal) {
                this.workerId = workerId;
                this.startSignal = startSignal;
                this.doneSignal = doneSignal;
            }
            
            @Override
            public void run() {
                try {
                    startSignal.await(); // Wait for start signal
                    
                    System.out.println("Worker " + workerId + " started");
                    Thread.sleep(1000 + workerId * 500); // Simulate work
                    System.out.println("Worker " + workerId + " completed");
                    
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    doneSignal.countDown(); // Signal completion
                }
            }
        }
    }
    
    // CyclicBarrier example
    static class CyclicBarrierExample {
        public void demonstrate() throws InterruptedException {
            System.out.println("=== CyclicBarrier Example ===");
            
            int numParticipants = 3;
            CyclicBarrier barrier = new CyclicBarrier(numParticipants, () -> {
                System.out.println("*** All participants reached barrier - proceeding! ***");
            });
            
            // Create participant threads
            for (int i = 1; i <= numParticipants; i++) {
                new Thread(new Participant(i, barrier)).start();
            }
            
            Thread.sleep(5000); // Let demonstration complete
            System.out.println("CyclicBarrier demonstration completed\n");
        }
        
        class Participant implements Runnable {
            private final int participantId;
            private final CyclicBarrier barrier;
            
            Participant(int participantId, CyclicBarrier barrier) {
                this.participantId = participantId;
                this.barrier = barrier;
            }
            
            @Override
            public void run() {
                try {
                    // Phase 1
                    System.out.println("Participant " + participantId + " working on phase 1");
                    Thread.sleep(1000 + participantId * 300);
                    System.out.println("Participant " + participantId + " completed phase 1");
                    
                    barrier.await(); // Wait for all to complete phase 1
                    
                    // Phase 2
                    System.out.println("Participant " + participantId + " working on phase 2");
                    Thread.sleep(800 + participantId * 200);
                    System.out.println("Participant " + participantId + " completed phase 2");
                    
                } catch (Exception e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
    
    // Semaphore example
    static class SemaphoreExample {
        public void demonstrate() throws InterruptedException {
            System.out.println("=== Semaphore Example ===");
            
            Semaphore semaphore = new Semaphore(2); // Allow 2 concurrent accesses
            
            // Create multiple threads trying to access limited resource
            for (int i = 1; i <= 5; i++) {
                new Thread(new ResourceUser(i, semaphore)).start();
            }
            
            Thread.sleep(8000); // Let demonstration complete
            System.out.println("Semaphore demonstration completed\n");
        }
        
        class ResourceUser implements Runnable {
            private final int userId;
            private final Semaphore semaphore;
            
            ResourceUser(int userId, Semaphore semaphore) {
                this.userId = userId;
                this.semaphore = semaphore;
            }
            
            @Override
            public void run() {
                try {
                    System.out.println("User " + userId + " requesting access...");
                    semaphore.acquire(); // Acquire permit
                    
                    System.out.println("User " + userId + " accessing limited resource");
                    Thread.sleep(2000); // Use resource
                    System.out.println("User " + userId + " finished using resource");
                    
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    semaphore.release(); // Release permit
                    System.out.println("User " + userId + " released access");
                }
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        new ProducerConsumerExample().demonstrate();
        new CountDownLatchExample().demonstrate();
        new CyclicBarrierExample().demonstrate();
        new SemaphoreExample().demonstrate();
    }
}

Thread Safety and Best Practices

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadSafetyExample {
    
    // Unsafe counter
    static class UnsafeCounter {
        private int count = 0;
        
        public void increment() {
            count++; // Not thread-safe!
        }
        
        public int getCount() {
            return count;
        }
    }
    
    // Safe counter using synchronization
    static class SafeCounter {
        private int count = 0;
        
        public synchronized void increment() {
            count++; // Thread-safe
        }
        
        public synchronized int getCount() {
            return count;
        }
    }
    
    // Atomic counter
    static class AtomicCounter {
        private final AtomicInteger count = new AtomicInteger(0);
        
        public void increment() {
            count.incrementAndGet(); // Thread-safe and lock-free
        }
        
        public int getCount() {
            return count.get();
        }
    }
    
    public static void demonstrateThreadSafety() throws InterruptedException {
        System.out.println("=== Thread Safety Demonstration ===");
        
        int numThreads = 10;
        int incrementsPerThread = 1000;
        int expectedTotal = numThreads * incrementsPerThread;
        
        // Test unsafe counter
        UnsafeCounter unsafeCounter = new UnsafeCounter();
        Thread[] unsafeThreads = new Thread[numThreads];
        
        for (int i = 0; i < numThreads; i++) {
            unsafeThreads[i] = new Thread(() -> {
                for (int j = 0; j < incrementsPerThread; j++) {
                    unsafeCounter.increment();
                }
            });
        }
        
        long startTime = System.currentTimeMillis();
        for (Thread thread : unsafeThreads) {
            thread.start();
        }
        for (Thread thread : unsafeThreads) {
            thread.join();
        }
        long unsafeTime = System.currentTimeMillis() - startTime;
        
        System.out.println("Unsafe counter result: " + unsafeCounter.getCount() + 
                         " (expected: " + expectedTotal + ") Time: " + unsafeTime + "ms");
        
        // Test safe counter
        SafeCounter safeCounter = new SafeCounter();
        Thread[] safeThreads = new Thread[numThreads];
        
        for (int i = 0; i < numThreads; i++) {
            safeThreads[i] = new Thread(() -> {
                for (int j = 0; j < incrementsPerThread; j++) {
                    safeCounter.increment();
                }
            });
        }
        
        startTime = System.currentTimeMillis();
        for (Thread thread : safeThreads) {
            thread.start();
        }
        for (Thread thread : safeThreads) {
            thread.join();
        }
        long safeTime = System.currentTimeMillis() - startTime;
        
        System.out.println("Safe counter result: " + safeCounter.getCount() + 
                         " (expected: " + expectedTotal + ") Time: " + safeTime + "ms");
        
        // Test atomic counter
        AtomicCounter atomicCounter = new AtomicCounter();
        Thread[] atomicThreads = new Thread[numThreads];
        
        for (int i = 0; i < numThreads; i++) {
            atomicThreads[i] = new Thread(() -> {
                for (int j = 0; j < incrementsPerThread; j++) {
                    atomicCounter.increment();
                }
            });
        }
        
        startTime = System.currentTimeMillis();
        for (Thread thread : atomicThreads) {
            thread.start();
        }
        for (Thread thread : atomicThreads) {
            thread.join();
        }
        long atomicTime = System.currentTimeMillis() - startTime;
        
        System.out.println("Atomic counter result: " + atomicCounter.getCount() + 
                         " (expected: " + expectedTotal + ") Time: " + atomicTime + "ms");
    }
    
    public static void threadSafetyBestPractices() {
        System.out.println("\n=== Thread Safety Best Practices ===");
        System.out.println("1. Use immutable objects when possible");
        System.out.println("2. Synchronize access to shared mutable state");
        System.out.println("3. Prefer atomic classes for simple operations");
        System.out.println("4. Use thread-safe collections (ConcurrentHashMap, etc.)");
        System.out.println("5. Minimize shared state between threads");
        System.out.println("6. Use ThreadLocal for thread-specific data");
        System.out.println("7. Avoid deadlocks by acquiring locks in consistent order");
        System.out.println("8. Use higher-level concurrency utilities (ExecutorService, etc.)");
        System.out.println("9. Handle InterruptedException properly");
        System.out.println("10. Test thoroughly with multiple threads");
    }
    
    public static void main(String[] args) throws InterruptedException {
        demonstrateThreadSafety();
        threadSafetyBestPractices();
    }
}

Summary

Java threads provide powerful concurrency capabilities:

Key Concepts:

  • Thread Creation: Extend Thread, implement Runnable, or use Callable
  • Thread States: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
  • Thread Properties: Name, priority, daemon status, thread groups
  • Communication: wait/notify, CountDownLatch, CyclicBarrier, Semaphore

Best Practices:

  • Prefer Runnable: More flexible than extending Thread class
  • Use Thread Pools: Better resource management than creating threads directly
  • Handle Interruption: Properly respond to interrupt signals
  • Ensure Thread Safety: Synchronize access to shared mutable state
  • Avoid Blocking: Use non-blocking alternatives when possible
  • Clean Shutdown: Properly terminate threads and clean up resources

Common Patterns:

  • Producer-Consumer: For data processing pipelines
  • Worker Pool: For parallel task execution
  • Master-Worker: For divide-and-conquer algorithms
  • Pipeline: For multi-stage data processing

Performance Considerations:

  • Context Switching: Overhead of switching between threads
  • Memory Usage: Each thread requires its own stack space
  • Synchronization Costs: Locks and atomic operations have overhead
  • Cache Effects: False sharing can degrade performance

Understanding threads is fundamental for building responsive, scalable Java applications that can effectively utilize modern multi-core processors.