Threads and Concurrency

Threads and Concurrency

Learn how to work with multiple threads in Java, understand synchronization, and build concurrent applications safely and efficiently.

Prerequisites: Understanding of OOP Concepts and Collections is recommended before diving into concurrency topics.

Why Learn Concurrency?

Modern applications often need to perform multiple tasks simultaneously:

  • Responsive UIs: Keep interfaces responsive while processing data
  • Server Applications: Handle multiple client requests concurrently
  • Performance: Utilize multiple CPU cores effectively
  • Real-world Scenarios: Most enterprise applications use concurrency

Thread Fundamentals

Creating and Running Threads

 1public class MyThread extends Thread {
 2    private String threadName;
 3    
 4    public MyThread(String name) {
 5        this.threadName = name;
 6    }
 7    
 8    @Override
 9    public void run() {
10        for (int i = 1; i <= 5; i++) {
11            System.out.println(threadName + " - Count: " + i);
12            try {
13                Thread.sleep(1000); // Sleep for 1 second
14            } catch (InterruptedException e) {
15                System.out.println(threadName + " was interrupted");
16                return;
17            }
18        }
19        System.out.println(threadName + " finished execution");
20    }
21    
22    public static void main(String[] args) {
23        MyThread thread1 = new MyThread("Worker-1");
24        MyThread thread2 = new MyThread("Worker-2");
25        
26        thread1.start(); // Start the first thread
27        thread2.start(); // Start the second thread
28        
29        System.out.println("Main thread continues...");
30    }
31}

What to Notice:

  • extends Thread approach limits inheritance flexibility
  • run() method contains the code executed by the thread
  • start() creates a new thread and calls run()
  • Multiple threads run concurrently
```java public class TaskRunner implements Runnable { private String taskName; private int iterations;
public TaskRunner(String name, int iterations) {
    this.taskName = name;
    this.iterations = iterations;
}

@Override
public void run() {
    System.out.println("Starting task: " + taskName);
    
    for (int i = 1; i <= iterations; i++) {
        System.out.println(taskName + " - Progress: " + i + "/" + iterations);
        
        // Simulate work
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            System.out.println(taskName + " was interrupted");
            Thread.currentThread().interrupt(); // Restore interrupt status
            return;
        }
    }
    
    System.out.println("Completed task: " + taskName);
}

public static void main(String[] args) {
    // Create tasks
    TaskRunner task1 = new TaskRunner("DataProcessing", 3);
    TaskRunner task2 = new TaskRunner("FileUpload", 4);
    
    // Create threads
    Thread thread1 = new Thread(task1);
    Thread thread2 = new Thread(task2);
    
    // Start execution
    thread1.start();
    thread2.start();
    
    // Wait for completion
    try {
        thread1.join(); // Wait for thread1 to finish
        thread2.join(); // Wait for thread2 to finish
    } catch (InterruptedException e) {
        System.out.println("Main thread was interrupted");
    }
    
    System.out.println("All tasks completed!");
}

}


<strong>What to Notice:</strong>
<ul>
<li><code>implements Runnable</code> is preferred (composition over inheritance)</li>
<li><code>join()</code> method waits for thread completion</li>
<li>Proper interrupt handling preserves interrupt status</li>
<li>More flexible than extending Thread class</li>
</ul>
### Thread States and Lifecycle

```java
public class ThreadStateDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            try {
                System.out.println("Worker thread starting...");
                Thread.sleep(2000); // TIMED_WAITING state
                System.out.println("Worker thread finishing...");
            } catch (InterruptedException e) {
                System.out.println("Worker was interrupted");
            }
        });
        
        // NEW state
        System.out.println("Initial state: " + worker.getState());
        
        worker.start();
        // RUNNABLE state
        System.out.println("After start: " + worker.getState());
        
        Thread.sleep(100); // Let worker start
        // TIMED_WAITING state (worker is sleeping)
        System.out.println("During sleep: " + worker.getState());
        
        worker.join(); // Wait for completion
        // TERMINATED state
        System.out.println("After completion: " + worker.getState());
    }
}

Thread States:

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

Synchronization and Thread Safety

The Need for Synchronization

 1public class CounterProblem {
 2    private static int counter = 0;
 3    
 4    public static void increment() {
 5        counter++; // This is NOT atomic!
 6        // Internally: read counter, add 1, write back
 7    }
 8    
 9    public static void main(String[] args) throws InterruptedException {
10        Thread[] threads = new Thread[10];
11        
12        // Create 10 threads, each incrementing 1000 times
13        for (int i = 0; i < 10; i++) {
14            threads[i] = new Thread(() -> {
15                for (int j = 0; j < 1000; j++) {
16                    increment();
17                }
18            });
19        }
20        
21        // Start all threads
22        for (Thread thread : threads) {
23            thread.start();
24        }
25        
26        // Wait for completion
27        for (Thread thread : threads) {
28            thread.join();
29        }
30        
31        System.out.println("Expected: 10000, Actual: " + counter);
32        // Result is usually less than 10000 due to race conditions!
33    }
34}

Synchronized Methods and Blocks

 1public class SafeCounter {
 2    private int count = 0;
 3    
 4    // Synchronized method
 5    public synchronized void increment() {
 6        count++; // Only one thread can execute this at a time
 7    }
 8    
 9    // Synchronized block with specific object
10    public void incrementWithBlock() {
11        synchronized(this) {
12            count++;
13        }
14    }
15    
16    // Read method should also be synchronized for consistency
17    public synchronized int getCount() {
18        return count;
19    }
20    
21    public static void main(String[] args) throws InterruptedException {
22        SafeCounter counter = new SafeCounter();
23        Thread[] threads = new Thread[10];
24        
25        for (int i = 0; i < 10; i++) {
26            threads[i] = new Thread(() -> {
27                for (int j = 0; j < 1000; j++) {
28                    counter.increment();
29                }
30            });
31        }
32        
33        for (Thread thread : threads) {
34            thread.start();
35        }
36        
37        for (Thread thread : threads) {
38            thread.join();
39        }
40        
41        System.out.println("Final count: " + counter.getCount());
42        // Now consistently outputs 10000
43    }
44}

What to Notice:

  • synchronized keyword ensures only one thread executes the method
  • Can synchronize on this, class objects, or any object
  • Both read and write operations should be synchronized
  • Synchronization has performance overhead

Locks and Advanced Synchronization

 1import java.util.concurrent.locks.ReentrantLock;
 2import java.util.concurrent.locks.ReadWriteLock;
 3import java.util.concurrent.locks.ReentrantReadWriteLock;
 4
 5public class AdvancedLocking {
 6    private final ReentrantLock lock = new ReentrantLock();
 7    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
 8    private int value = 0;
 9    
10    // Using ReentrantLock for more control
11    public void updateWithLock(int newValue) {
12        lock.lock(); // Explicit lock
13        try {
14            // Critical section
15            System.out.println("Updating value from " + value + " to " + newValue);
16            Thread.sleep(100); // Simulate work
17            value = newValue;
18        } catch (InterruptedException e) {
19            Thread.currentThread().interrupt();
20        } finally {
21            lock.unlock(); // Always unlock in finally block
22        }
23    }
24    
25    // Using ReadWriteLock for better read performance
26    public int readValue() {
27        readWriteLock.readLock().lock();
28        try {
29            return value; // Multiple readers can access simultaneously
30        } finally {
31            readWriteLock.readLock().unlock();
32        }
33    }
34    
35    public void writeValue(int newValue) {
36        readWriteLock.writeLock().lock();
37        try {
38            value = newValue; // Only one writer allowed
39        } finally {
40            readWriteLock.writeLock().unlock();
41        }
42    }
43    
44    public static void main(String[] args) throws InterruptedException {
45        AdvancedLocking demo = new AdvancedLocking();
46        
47        // Create multiple reader threads
48        for (int i = 0; i < 5; i++) {
49            final int threadId = i;
50            new Thread(() -> {
51                for (int j = 0; j < 3; j++) {
52                    int val = demo.readValue();
53                    System.out.println("Reader " + threadId + " read: " + val);
54                    try { Thread.sleep(50); } catch (InterruptedException e) {}
55                }
56            }).start();
57        }
58        
59        // Create writer threads
60        for (int i = 0; i < 2; i++) {
61            final int threadId = i;
62            new Thread(() -> {
63                demo.writeValue(threadId * 10);
64                System.out.println("Writer " + threadId + " finished");
65            }).start();
66        }
67        
68        Thread.sleep(2000); // Let threads complete
69    }
70}

Producer-Consumer Pattern

A classic concurrency pattern for coordinating threads:

 1import java.util.LinkedList;
 2import java.util.Queue;
 3
 4public class ProducerConsumer {
 5    private final Queue<Integer> buffer = new LinkedList<>();
 6    private final int capacity = 5;
 7    private final Object lock = new Object();
 8    
 9    public void produce() throws InterruptedException {
10        int value = 0;
11        while (true) {
12            synchronized (lock) {
13                // Wait while buffer is full
14                while (buffer.size() == capacity) {
15                    System.out.println("Buffer full, producer waiting...");
16                    lock.wait(); // Release lock and wait
17                }
18                
19                // Add item to buffer
20                buffer.offer(value);
21                System.out.println("Produced: " + value + " (Buffer size: " + buffer.size() + ")");
22                value++;
23                
24                // Notify waiting consumers
25                lock.notifyAll();
26            }
27            
28            Thread.sleep(500); // Simulate production time
29        }
30    }
31    
32    public void consume() throws InterruptedException {
33        while (true) {
34            synchronized (lock) {
35                // Wait while buffer is empty
36                while (buffer.isEmpty()) {
37                    System.out.println("Buffer empty, consumer waiting...");
38                    lock.wait(); // Release lock and wait
39                }
40                
41                // Remove item from buffer
42                int value = buffer.poll();
43                System.out.println("Consumed: " + value + " (Buffer size: " + buffer.size() + ")");
44                
45                // Notify waiting producers
46                lock.notifyAll();
47            }
48            
49            Thread.sleep(1000); // Simulate consumption time
50        }
51    }
52    
53    public static void main(String[] args) {
54        ProducerConsumer pc = new ProducerConsumer();
55        
56        // Create producer thread
57        Thread producer = new Thread(() -> {
58            try {
59                pc.produce();
60            } catch (InterruptedException e) {
61                Thread.currentThread().interrupt();
62            }
63        });
64        
65        // Create consumer thread
66        Thread consumer = new Thread(() -> {
67            try {
68                pc.consume();
69            } catch (InterruptedException e) {
70                Thread.currentThread().interrupt();
71            }
72        });
73        
74        producer.start();
75        consumer.start();
76        
77        // Let them run for a while
78        try {
79            Thread.sleep(10000);
80            producer.interrupt();
81            consumer.interrupt();
82        } catch (InterruptedException e) {
83            Thread.currentThread().interrupt();
84        }
85    }
86}

What to Notice:

  • wait() releases the lock and suspends the thread
  • notifyAll() wakes up all waiting threads
  • Always use wait() in a loop to handle spurious wakeups
  • Proper synchronization prevents race conditions

Modern Concurrency Utilities

ExecutorService for Thread Management

 1import java.util.concurrent.*;
 2import java.util.List;
 3import java.util.ArrayList;
 4
 5public class ExecutorServiceDemo {
 6    public static void main(String[] args) throws InterruptedException, ExecutionException {
 7        // Create thread pool with 3 threads
 8        ExecutorService executor = Executors.newFixedThreadPool(3);
 9        
10        try {
11            // Submit tasks that return results
12            List<Future<String>> futures = new ArrayList<>();
13            
14            for (int i = 1; i <= 5; i++) {
15                final int taskId = i;
16                Future<String> future = executor.submit(() -> {
17                    Thread.sleep(1000 + (taskId * 200)); // Simulate varying work
18                    return "Task " + taskId + " completed by " + Thread.currentThread().getName();
19                });
20                futures.add(future);
21            }
22            
23            // Collect results
24            for (int i = 0; i < futures.size(); i++) {
25                String result = futures.get(i).get(); // Blocking call
26                System.out.println("Result " + (i + 1) + ": " + result);
27            }
28            
29            // Submit a task with timeout
30            Future<String> timeoutTask = executor.submit(() -> {
31                Thread.sleep(5000); // Long-running task
32                return "Long task completed";
33            });
34            
35            try {
36                String result = timeoutTask.get(2, TimeUnit.SECONDS);
37                System.out.println(result);
38            } catch (TimeoutException e) {
39                System.out.println("Task timed out!");
40                timeoutTask.cancel(true); // Interrupt the task
41            }
42            
43        } finally {
44            // Always shutdown executor
45            executor.shutdown();
46            if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
47                executor.shutdownNow();
48            }
49        }
50    }
51}

BlockingQueue for Thread-Safe Communication

 1import java.util.concurrent.BlockingQueue;
 2import java.util.concurrent.LinkedBlockingQueue;
 3import java.util.concurrent.TimeUnit;
 4
 5public class BlockingQueueDemo {
 6    public static void main(String[] args) throws InterruptedException {
 7        BlockingQueue<String> queue = new LinkedBlockingQueue<>(3); // Capacity of 3
 8        
 9        // Producer thread
10        Thread producer = new Thread(() -> {
11            try {
12                for (int i = 1; i <= 6; i++) {
13                    String item = "Item-" + i;
14                    System.out.println("Producing: " + item);
15                    queue.put(item); // Blocks if queue is full
16                    System.out.println("Produced: " + item + " (Queue size: " + queue.size() + ")");
17                    Thread.sleep(500);
18                }
19            } catch (InterruptedException e) {
20                Thread.currentThread().interrupt();
21            }
22        });
23        
24        // Consumer thread
25        Thread consumer = new Thread(() -> {
26            try {
27                while (true) {
28                    // Take with timeout to avoid infinite blocking
29                    String item = queue.poll(2, TimeUnit.SECONDS);
30                    if (item == null) {
31                        System.out.println("No item available, consumer stopping");
32                        break;
33                    }
34                    System.out.println("Consuming: " + item);
35                    Thread.sleep(1000); // Slower consumption
36                    System.out.println("Consumed: " + item + " (Queue size: " + queue.size() + ")");
37                }
38            } catch (InterruptedException e) {
39                Thread.currentThread().interrupt();
40            }
41        });
42        
43        producer.start();
44        Thread.sleep(1000); // Let producer get ahead
45        consumer.start();
46        
47        producer.join();
48        consumer.join();
49        
50        System.out.println("Final queue size: " + queue.size());
51    }
52}

Best Practices and Common Pitfalls

Thread-Safe Singleton Pattern

 1public class ThreadSafeSingleton {
 2    // Volatile ensures visibility of changes across threads
 3    private static volatile ThreadSafeSingleton instance;
 4    private String data;
 5    
 6    private ThreadSafeSingleton() {
 7        // Expensive initialization
 8        try {
 9            Thread.sleep(100);
10            data = "Initialized at " + System.currentTimeMillis();
11        } catch (InterruptedException e) {
12            Thread.currentThread().interrupt();
13        }
14    }
15    
16    // Double-checked locking pattern
17    public static ThreadSafeSingleton getInstance() {
18        if (instance == null) { // First check (no locking)
19            synchronized (ThreadSafeSingleton.class) {
20                if (instance == null) { // Second check (with locking)
21                    instance = new ThreadSafeSingleton();
22                }
23            }
24        }
25        return instance;
26    }
27    
28    public String getData() {
29        return data;
30    }
31    
32    public static void main(String[] args) throws InterruptedException {
33        Thread[] threads = new Thread[10];
34        
35        for (int i = 0; i < 10; i++) {
36            threads[i] = new Thread(() -> {
37                ThreadSafeSingleton singleton = ThreadSafeSingleton.getInstance();
38                System.out.println("Instance: " + singleton.hashCode() + 
39                                 ", Data: " + singleton.getData());
40            });
41        }
42        
43        for (Thread thread : threads) {
44            thread.start();
45        }
46        
47        for (Thread thread : threads) {
48            thread.join();
49        }
50    }
51}

Avoiding Deadlocks

 1public class DeadlockPrevention {
 2    private final Object lock1 = new Object();
 3    private final Object lock2 = new Object();
 4    
 5    // BAD: Can cause deadlock
 6    public void badMethod1() {
 7        synchronized (lock1) {
 8            System.out.println("Method1: acquired lock1");
 9            synchronized (lock2) {
10                System.out.println("Method1: acquired lock2");
11            }
12        }
13    }
14    
15    public void badMethod2() {
16        synchronized (lock2) {
17            System.out.println("Method2: acquired lock2");
18            synchronized (lock1) {
19                System.out.println("Method2: acquired lock1");
20            }
21        }
22    }
23    
24    // GOOD: Consistent lock ordering prevents deadlock
25    public void goodMethod1() {
26        synchronized (lock1) {
27            System.out.println("GoodMethod1: acquired lock1");
28            synchronized (lock2) {
29                System.out.println("GoodMethod1: acquired lock2");
30            }
31        }
32    }
33    
34    public void goodMethod2() {
35        synchronized (lock1) { // Same order as goodMethod1
36            System.out.println("GoodMethod2: acquired lock1");
37            synchronized (lock2) {
38                System.out.println("GoodMethod2: acquired lock2");
39            }
40        }
41    }
42    
43    public static void main(String[] args) {
44        DeadlockPrevention demo = new DeadlockPrevention();
45        
46        // This can deadlock (commented out for safety)
47        /*
48        new Thread(demo::badMethod1).start();
49        new Thread(demo::badMethod2).start();
50        */
51        
52        // This won't deadlock
53        new Thread(demo::goodMethod1).start();
54        new Thread(demo::goodMethod2).start();
55    }
56}

Key Takeaways

Thread Safety Guidelines:

  • Always synchronize access to shared mutable data
  • Use volatile for simple flags and state variables
  • Prefer higher-level concurrency utilities over low-level synchronization
  • Follow consistent lock ordering to prevent deadlocks
  • Use thread pools instead of creating threads manually

Common Pitfalls:

  • Forgetting to synchronize read operations
  • Calling run() instead of start()
  • Not handling InterruptedException properly
  • Creating too many threads (use thread pools)
  • Shared mutable state without proper synchronization

Next Steps

  • Practice with java.util.concurrent package utilities
  • Learn about CompletableFuture for asynchronous programming
  • Explore parallel streams for data processing
  • Study lock-free programming techniques
  • Understand memory models and happens-before relationships

Concurrency is challenging but essential for modern Java development. Start with simple examples and gradually work up to more complex patterns as you gain experience.