75% complete
Java Multithreading and Concurrency
Multithreading is a powerful feature in Java that allows concurrent execution of two or more parts of a program, maximizing CPU utilization and improving application performance. Understanding multithreading is essential for developing responsive applications, especially those that involve intensive I/O operations, complex computations, or serving multiple users simultaneously.
What You'll Learn
- Creating and managing threads in Java
- Synchronization and thread safety
- Using locks, semaphores, and other concurrency utilities
- Working with the Executor Framework
- Handling thread coordination and communication
- Understanding deadlocks and race conditions
- Modern concurrency with CompletableFuture
Java Threads Fundamentals
A thread is the smallest unit of execution in a program. Java has built-in support for multithreaded programming through the Thread class and the Runnable interface.
Process vs Thread
A process is an independent program with its own memory space, while threads are lightweight parts of a process that share the same memory space. Multiple threads within a process can execute concurrently.
Thread Lifecycle
Java threads go through various states: New, Runnable, Blocked, Waiting, Timed Waiting, and Terminated. Understanding this lifecycle is key to effective thread management.
Creating Threads in Java
There are two primary ways to create threads in Java:
1. Extending the Thread Class
1public class MyThread extends Thread {2 @Override3 public void run() {4 // Code to be executed in this thread5 System.out.println("Thread is running: " + Thread.currentThread().getName());6 }78 public static void main(String[] args) {9 MyThread thread = new MyThread();10 thread.start(); // Start the thread11 }12}
2. Implementing the Runnable Interface (Recommended)
1public class MyRunnable implements Runnable {2 @Override3 public void run() {4 // Code to be executed in this thread5 System.out.println("Thread is running: " + Thread.currentThread().getName());6 }78 public static void main(String[] args) {9 MyRunnable myRunnable = new MyRunnable();10 Thread thread = new Thread(myRunnable);11 thread.start(); // Start the thread12 }13}
Why Prefer Runnable over Thread?
- Separation of concerns: Separates the task (what to run) from the thread (how to run).
- Reusability: The same Runnable can be used with multiple threads.
- Inheritance flexibility: Your class can still extend another class while implementing Runnable.
- Framework compatibility: Most Java concurrency utilities work with Runnable, not Thread.
Thread Operations and States
Method/State | Description |
---|---|
start() | Begins thread execution by calling the run() method |
run() | Contains the code that constitutes the thread's task |
sleep() | Pauses thread execution for a specified time |
join() | Waits for the thread to die before continuing execution |
yield() | Suggests the scheduler to switch to other threads |
interrupt() | Interrupts a thread in waiting/sleeping state |
Thread Synchronization
When multiple threads access shared resources, synchronization is necessary to prevent race conditions and ensure data consistency.
The Synchronized Keyword
Java provides the synchronized
keyword to create synchronized blocks and methods:
1public class Counter {2 private int count = 0;34 // Synchronized method - only one thread can execute this at a time5 public synchronized void increment() {6 count++;7 }89 // Synchronized block - locks only when necessary10 public void incrementWithBlock() {11 // Other non-synchronized code can run in parallel1213 synchronized(this) {14 count++; // Only this part is synchronized15 }1617 // More non-synchronized code18 }1920 public int getCount() {21 return count;22 }23}
Be careful with synchronization! While it prevents data corruption, excessive synchronization can lead to performance bottlenecks or even deadlocks.
Common Thread Safety Issues
Race Conditions
Occur when the result depends on the sequence or timing of uncontrollable events. Multiple threads updating a shared variable without proper synchronization is a common example.
Deadlocks
Happen when two or more threads are blocked forever, each waiting for resources held by the others. Proper lock ordering and timeout mechanisms can help prevent deadlocks.
Thread Starvation
Occurs when a thread is perpetually denied access to resources it needs. This can happen when high-priority threads consistently take all available CPU time.
Memory Consistency Errors
Happen when different threads have inconsistent views of the same data. The volatile keyword and proper synchronization can help prevent these issues.
Java Concurrency Utilities
Since Java 5, the java.util.concurrent
package provides high-level concurrency utilities that simplify multithreaded programming.
Executor Framework
The Executor framework separates task submission from execution, providing a more flexible and powerful alternative to directly creating threads.
1import java.util.concurrent.ExecutorService;2import java.util.concurrent.Executors;34public class ExecutorExample {5 public static void main(String[] args) {6 // Create a fixed thread pool with 5 threads7 ExecutorService executor = Executors.newFixedThreadPool(5);89 // Submit tasks to the executor10 for (int i = 0; i < 10; i++) {11 final int taskId = i;12 executor.submit(() -> {13 System.out.println("Task " + taskId + " executed by " +14 Thread.currentThread().getName());15 });16 }1718 // Shutdown the executor when no longer needed19 executor.shutdown();20 }21}
Thread-Safe Collections
Java provides several thread-safe collection classes:
Collection Type | Thread-Safe Implementations |
---|---|
List | Vector, CopyOnWriteArrayList, Collections.synchronizedList() |
Map | Hashtable, ConcurrentHashMap, Collections.synchronizedMap() |
Set | CopyOnWriteArraySet, Collections.synchronizedSet() |
Queue | BlockingQueue implementations (ArrayBlockingQueue, LinkedBlockingQueue) |
Example using ConcurrentHashMap:
1import java.util.concurrent.ConcurrentHashMap;23public class ConcurrentMapExample {4 public static void main(String[] args) {5 ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();67 // Multiple threads can safely update the map concurrently8 map.put("one", 1);9 map.put("two", 2);1011 // Thread-safe operations12 map.computeIfAbsent("three", k -> 3);13 map.computeIfPresent("one", (k, v) -> v + 10);1415 System.out.println(map); // Output: {one=11, two=2, three=3}16 }17}
Locks and Atomic Variables
Java provides more flexible locking mechanisms than synchronized blocks:
1import java.util.concurrent.locks.ReentrantLock;23public class LockExample {4 private final ReentrantLock lock = new ReentrantLock();5 private int count = 0;67 public void increment() {8 lock.lock(); // Acquire the lock9 try {10 count++;11 } finally {12 lock.unlock(); // Always release the lock in a finally block13 }14 }1516 public int getCount() {17 lock.lock();18 try {19 return count;20 } finally {21 lock.unlock();22 }23 }24}
Atomic variables provide thread-safe operations without explicit locking:
1import java.util.concurrent.atomic.AtomicInteger;23public class AtomicExample {4 private AtomicInteger count = new AtomicInteger(0);56 public void increment() {7 count.incrementAndGet(); // Atomic operation, no explicit locking needed8 }910 public int getCount() {11 return count.get();12 }13}
Thread Communication
Threads often need to communicate with each other to coordinate their activities.
Wait and Notify
The traditional mechanism for thread communication uses wait()
, notify()
, and notifyAll()
methods:
1public class MessageQueue {2 private String message;3 private boolean isEmpty = true;45 public synchronized String receive() throws InterruptedException {6 // Wait until a message is available7 while (isEmpty) {8 wait(); // Release lock and wait to be notified9 }1011 // Message received12 isEmpty = true;13 notifyAll(); // Notify producers that queue is empty14 return message;15 }1617 public synchronized void send(String message) throws InterruptedException {18 // Wait until the queue is empty19 while (!isEmpty) {20 wait(); // Release lock and wait to be notified21 }2223 // Send the message24 this.message = message;25 isEmpty = false;26 notifyAll(); // Notify consumers that message is available27 }28}
Blocking Queues
For producer-consumer scenarios, blocking queues provide a more convenient alternative:
1import java.util.concurrent.BlockingQueue;2import java.util.concurrent.LinkedBlockingQueue;34public class ProducerConsumerExample {5 public static void main(String[] args) {6 // Create a blocking queue with capacity 107 BlockingQueue<String> queue = new LinkedBlockingQueue<>(10);89 // Producer thread10 Thread producer = new Thread(() -> {11 try {12 for (int i = 0; i < 20; i++) {13 String message = "Message " + i;14 queue.put(message); // Blocks if queue is full15 System.out.println("Produced: " + message);16 Thread.sleep(100);17 }18 } catch (InterruptedException e) {19 Thread.currentThread().interrupt();20 }21 });2223 // Consumer thread24 Thread consumer = new Thread(() -> {25 try {26 for (int i = 0; i < 20; i++) {27 String message = queue.take(); // Blocks if queue is empty28 System.out.println("Consumed: " + message);29 Thread.sleep(200);30 }31 } catch (InterruptedException e) {32 Thread.currentThread().interrupt();33 }34 });3536 producer.start();37 consumer.start();38 }39}
Modern Concurrency (Java 8+)
CompletableFuture
Introduced in Java 8, CompletableFuture provides a way to write asynchronous, non-blocking code:
1import java.util.concurrent.CompletableFuture;2import java.util.concurrent.ExecutionException;34public class CompletableFutureExample {5 public static void main(String[] args) throws ExecutionException, InterruptedException {6 // Create a CompletableFuture7 CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {8 try {9 Thread.sleep(1000); // Simulate long-running task10 } catch (InterruptedException e) {11 Thread.currentThread().interrupt();12 }13 return "Result from async computation";14 });1516 // Add a callback to be executed when the future completes17 future.thenAccept(result -> System.out.println("Got result: " + result));1819 // Chain multiple operations20 CompletableFuture<String> chainedFuture = future21 .thenApply(result -> result + " - processed")22 .thenApply(String::toUpperCase);2324 // Wait for the result25 System.out.println("Final result: " + chainedFuture.get());2627 // Combining multiple futures28 CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");29 CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");3031 CompletableFuture<String> combined = future1.thenCombine(future2, (s1, s2) -> s1 + " " + s2);32 System.out.println(combined.get()); // Output: Hello World33 }34}
Best Practices for Multithreaded Programming
Thread Safety Guidelines
- Minimize shared mutable state: The less shared data, the fewer synchronization issues.
- Prefer immutability: Immutable objects are inherently thread-safe.
- Use thread-safe collections: When sharing collections between threads.
- Keep synchronization blocks small: Synchronize only what's necessary.
- Prefer higher-level concurrency utilities: Use Executors, BlockingQueue, etc. instead of raw threads and synchronization.
- Document thread safety: Clearly indicate whether your classes are thread-safe or not.
- Test thoroughly: Concurrency bugs can be difficult to reproduce.
Performance Considerations
While multithreading can improve performance, it also comes with costs:
Thread Creation Overhead
Creating threads is expensive. Use thread pools to reuse threads for multiple tasks.
Context Switching
Switching between threads has overhead. Too many threads can lead to excessive context switching.
Synchronization Overhead
Locks and synchronization add overhead and can cause contention between threads.
Memory Usage
Each thread requires memory for its stack. Too many threads can lead to memory issues.
Conclusion
Multithreading in Java is a powerful tool for improving application performance and responsiveness. By understanding the core concepts, using appropriate synchronization mechanisms, and leveraging the rich concurrency utilities provided by Java, you can write efficient and correct multithreaded applications.
Remember that multithreaded programming introduces complexity and potential issues that don't exist in single-threaded code. Always approach concurrent programming with care, follow best practices, and thoroughly test your applications.
Related Tutorials
Learn how to handle errors and exceptions in Java.
Learn moreWorking with Java Collections Framework and data structures.
Learn moreLearn how to read and write files in Java.
Learn more