Learning Objectives
Multithreading Fundamentals
Understand threads, concurrency, and thread lifecycle
Thread Synchronization
Master synchronization and avoid race conditions
Concurrency Utilities
Learn Executor framework and modern concurrency tools
Thread Safety
Build thread-safe applications and avoid common pitfalls
Introduction to Multithreading
What is Multithreading?
Multithreading is the ability of a program to execute multiple threads concurrently, allowing it to perform multiple tasks simultaneously. This is particularly useful for improving performance and responsiveness in applications.
Threads vs C Threads
C
Manual thread management, POSIX threads (pthreads), manual synchronization
Java
Built-in thread support, automatic memory management, rich concurrency utilities
Benefits of Multithreading
Improved Performance
Better CPU utilization
Responsiveness
UI remains responsive during long operations
Resource Sharing
Threads can share memory and resources
Economy
Creating threads is cheaper than creating processes
Creating Threads
Method 1: Extending Thread Class
public class MyThread extends Thread {
private String threadName;
public MyThread(String name) {
this.threadName = name;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(threadName + " executing: " + i);
try {
Thread.sleep(1000); // Sleep for 1 second
} catch (InterruptedException e) {
System.out.println(threadName + " interrupted");
return;
}
}
System.out.println(threadName + " finished");
}
}
// Usage
MyThread thread1 = new MyThread("Thread-1");
MyThread thread2 = new MyThread("Thread-2");
thread1.start(); // Start the thread
thread2.start(); // Start another thread
Method 2: Implementing Runnable Interface
public class MyRunnable implements Runnable {
private String threadName;
public MyRunnable(String name) {
this.threadName = name;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(threadName + " executing: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(threadName + " interrupted");
return;
}
}
System.out.println(threadName + " finished");
}
}
// Usage
Thread thread1 = new Thread(new MyRunnable("Thread-1"));
Thread thread2 = new Thread(new MyRunnable("Thread-2"));
thread1.start();
thread2.start();
Method 3: Lambda Expressions (Java 8+)
// Using lambda expressions
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Lambda Thread executing: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Lambda Thread interrupted");
return;
}
}
System.out.println("Lambda Thread finished");
});
thread1.start();
Thread Synchronization
The Problem: Race Conditions
When multiple threads access shared data simultaneously, race conditions can occur.
public class Counter {
private int count = 0;
public void increment() {
count++; // This is not atomic!
}
public int getCount() {
return count;
}
}
public class RaceConditionDemo {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// Create multiple threads that increment the counter
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
}
// Start all threads
for (Thread thread : threads) {
thread.start();
}
// Wait for all threads to complete
for (Thread thread : threads) {
thread.join();
}
// Expected: 10000, but might get different results due to race conditions
System.out.println("Final count: " + counter.getCount());
}
}
Solution 1: Synchronized Methods
public class SynchronizedCounter {
private int count = 0;
// Synchronized method - only one thread can execute this at a time
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
// Synchronized block - more granular control
public void incrementBy(int amount) {
synchronized (this) {
count += amount;
}
}
}
Solution 2: Volatile Keyword
public class VolatileDemo {
// Volatile ensures visibility across threads
private volatile boolean running = true;
public void stop() {
running = false;
}
public void run() {
while (running) {
// Do some work
System.out.println("Running...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
System.out.println("Stopped");
}
}
Modern Concurrency Utilities
Executor Framework
The Executor framework provides a higher-level replacement for working with threads directly.
import java.util.concurrent.*;
public class ExecutorDemo {
public static void main(String[] args) {
// Single thread executor
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// Fixed thread pool
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// Cached thread pool
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// Submit tasks
Future future1 = singleThreadExecutor.submit(() -> {
Thread.sleep(1000);
return "Task 1 completed";
});
Future future2 = fixedThreadPool.submit(() -> {
Thread.sleep(2000);
return "Task 2 completed";
});
// Get results
try {
System.out.println(future1.get()); // Waits for completion
System.out.println(future2.get(5, TimeUnit.SECONDS)); // With timeout
} catch (Exception e) {
e.printStackTrace();
}
// Shutdown executors
singleThreadExecutor.shutdown();
fixedThreadPool.shutdown();
cachedThreadPool.shutdown();
}
}
CompletableFuture (Java 8+)
CompletableFuture provides a way to write asynchronous, non-blocking code.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureDemo {
public static void main(String[] args) {
// Simple async task
CompletableFuture future1 = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "Hello";
});
// Chain operations
CompletableFuture future2 = future1
.thenApply(str -> str + " World")
.thenApply(str -> str + "!")
.thenApply(String::toUpperCase);
// Handle completion
future2.thenAccept(result -> System.out.println("Result: " + result));
// Combine multiple futures
CompletableFuture future3 = CompletableFuture.supplyAsync(() -> "Async");
CompletableFuture future4 = CompletableFuture.supplyAsync(() -> "Task");
CompletableFuture combined = future3.thenCombine(future4, (a, b) -> a + " " + b);
combined.thenAccept(result -> System.out.println("Combined: " + result));
}
}
Concurrent Collections
Thread-safe collections designed for concurrent access.
import java.util.concurrent.*;
public class ConcurrentCollectionsDemo {
public static void main(String[] args) {
// Thread-safe list
List concurrentList = new CopyOnWriteArrayList<>();
// Thread-safe map
Map concurrentMap = new ConcurrentHashMap<>();
// Thread-safe queue
BlockingQueue blockingQueue = new LinkedBlockingQueue<>();
// Thread-safe set
Set concurrentSet = ConcurrentHashMap.newKeySet();
// Add elements concurrently
ExecutorService executor = Executors.newFixedThreadPool(3);
// Add to concurrent list
for (int i = 0; i < 3; i++) {
final int threadId = i;
executor.submit(() -> {
for (int j = 0; j < 5; j++) {
concurrentList.add("Thread-" + threadId + "-Item-" + j);
}
});
}
executor.shutdown();
try {
executor.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Concurrent list size: " + concurrentList.size());
System.out.println("Concurrent map size: " + concurrentMap.size());
}
}
Practical Examples
Producer-Consumer with Blocking Queue
import java.util.concurrent.*;
public class ProducerConsumerWithBlockingQueue {
private final BlockingQueue queue;
private final int maxSize;
public ProducerConsumerWithBlockingQueue(int maxSize) {
this.queue = new ArrayBlockingQueue<>(maxSize);
this.maxSize = maxSize;
}
public void start() {
// Start producer
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
queue.put(i); // Blocks if queue is full
System.out.println("Produced: " + i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// Start consumer
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
int item = queue.take(); // Blocks if queue is empty
System.out.println("Consumed: " + item);
Thread.sleep(200);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
try {
producer.join();
consumer.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Key Differences from C
Concept | C | Java |
---|---|---|
Thread Creation | pthread_create, manual management | Built-in Thread class, automatic management |
Synchronization | Mutexes, semaphores, condition variables | synchronized, wait/notify, concurrent utilities |
Memory Management | Manual allocation/deallocation | Automatic garbage collection |
Thread Safety | Manual implementation | Built-in thread-safe collections |
Error Handling | Return codes, errno | Exception handling |
Resource Management | Manual cleanup | Automatic with try-with-resources |
Summary
In this module, you've learned:
- ? How to create and manage threads in Java
- ? Thread synchronization techniques and avoiding race conditions
- ? Modern concurrency utilities like Executor framework and CompletableFuture
- ? Thread-safe collections and atomic operations
- ? Best practices for multithreaded programming
- ? Key differences between Java threading and C threading approaches