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 flexibilityrun()
method contains the code executed by the threadstart()
creates a new thread and callsrun()
- Multiple threads run concurrently
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 threadnotifyAll()
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 ofstart()
- 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.