Multithreading Concepts#
Lecture 30#
Java Programming (4343203)
Diploma in ICT - Semester IV
Gujarat Technological University
layout: default#
Learning Objectives#
By the end of this lecture, you will be able to:
- ๐งต Understand the fundamental concepts of threads and multithreading
- ๐ Differentiate between processes and threads in operating systems
- ๐ Explain the thread lifecycle and state transitions
- โก Identify advantages and challenges of multithreading
- ๐ฏ Recognize multithreading scenarios and use cases
- ๐ง Understand concurrency vs parallelism concepts
- ๐๏ธ Analyze thread scheduling and context switching
layout: default#
What is Multithreading?#
Definition#
Multithreading is the ability of a program to execute multiple threads concurrently within a single process, allowing different parts of the program to run simultaneously.
Key Concepts#
Thread#
- Lightweight subprocess that can execute concurrently
- Shares memory with other threads in the same process
- Has its own stack and program counter
- Smallest unit of execution scheduled by the OS
Process vs Thread#
Process = Program in execution
โโโ Memory Space (Code, Data, Heap)
โโโ System Resources (Files, Network)
โโโ Threads
โโโ Thread 1 (Main Thread)
โ โโโ Stack
โ โโโ Program Counter
โ โโโ Registers
โโโ Thread 2
โ โโโ Stack
โ โโโ Program Counter
โ โโโ Registers
โโโ Thread N...
Real-world Analogy#
Think of multithreading like a restaurant kitchen:
- Process = The entire kitchen
- Threads = Individual cooks working simultaneously
- Shared Memory = Common ingredients, equipment, orders
- Private Stack = Each cook’s workspace and tools
- Coordination = Head chef managing the workflow
Sequential vs Multithreaded Execution#
Sequential (Single-threaded) Execution#
public class SequentialExample {
public static void main(String[] args) {
System.out.println("Starting sequential execution");
// Task 1: Calculate sum (takes time)
long sum = 0;
for (int i = 1; i <= 1000000; i++) {
sum += i;
}
System.out.println("Sum calculated: " + sum);
// Task 2: Print numbers (takes time)
for (int i = 1; i <= 5; i++) {
System.out.println("Number: " + i);
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
// Task 3: Download simulation (takes time)
System.out.println("Simulating file download...");
try { Thread.sleep(3000); } catch (InterruptedException e) {}
System.out.println("Download completed");
System.out.println("All tasks completed sequentially");
}
}
// Total time: ~8 seconds (tasks run one after another)
Multithreaded Execution#
public class MultithreadedExample {
public static void main(String[] args) {
System.out.println("Starting multithreaded execution");
// Task 1: Calculate sum in separate thread
Thread calculationThread = new Thread(() -> {
long sum = 0;
for (int i = 1; i <= 1000000; i++) {
sum += i;
}
System.out.println("Sum calculated: " + sum);
});
// Task 2: Print numbers in separate thread
Thread printingThread = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Number: " + i);
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
});
// Task 3: Download simulation in separate thread
Thread downloadThread = new Thread(() -> {
System.out.println("Simulating file download...");
try { Thread.sleep(3000); } catch (InterruptedException e) {}
System.out.println("Download completed");
});
// Start all threads (they run concurrently)
calculationThread.start();
printingThread.start();
downloadThread.start();
System.out.println("All tasks started concurrently");
}
}
// Total time: ~3 seconds (tasks run simultaneously)
layout: default#
Process vs Thread#
Process Characteristics#
Definition#
A process is a program in execution - an instance of a program loaded into memory and managed by the operating system.
Process Features:#
- Separate Memory Space: Each process has its own virtual address space
- Independent Execution: Processes run independently of each other
- Resource Isolation: Cannot directly access another process’s memory
- Communication: Inter-Process Communication (IPC) required (pipes, shared memory, sockets)
- Creation Overhead: Creating new process is expensive
- Protection: OS provides strong isolation between processes
Process Example:#
// Each Java application runs as a separate process
public class ProcessExample {
public static void main(String[] args) {
System.out.println("Process ID: " + ProcessHandle.current().pid());
System.out.println("Available processors: " +
Runtime.getRuntime().availableProcessors());
// Launch another process
try {
ProcessBuilder pb = new ProcessBuilder("notepad.exe");
Process process = pb.start();
System.out.println("Started process: " + process.pid());
} catch (IOException e) {
System.out.println("Error starting process: " + e.getMessage());
}
}
}
Process Memory Layout:#
Process A Memory Process B Memory
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ Code Segment โ โ Code Segment โ
โ Data Segment โ โ Data Segment โ
โ Heap โ โ Heap โ
โ Stack โ โ Stack โ
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ โ
Isolated - Cannot directly access each other
Thread Characteristics#
Definition#
A thread is a lightweight unit of execution within a process - multiple threads can exist within a single process.
Thread Features:#
- Shared Memory Space: All threads share the same memory space (heap)
- Lightweight: Creating threads is less expensive than processes
- Fast Communication: Threads communicate through shared memory
- Independent Execution Flow: Each thread has its own stack and program counter
- Shared Resources: Share file handles, network connections, etc.
- Synchronization Needed: Requires coordination to avoid conflicts
Thread Example:#
public class ThreadExample {
private static int sharedCounter = 0; // Shared by all threads
public static void main(String[] args) {
System.out.println("Main thread: " + Thread.currentThread().getName());
// Create multiple threads that share memory
for (int i = 1; i <= 3; i++) {
Thread worker = new Thread(new Worker(i));
worker.start();
}
}
static class Worker implements Runnable {
private int workerId;
public Worker(int workerId) {
this.workerId = workerId;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
// All threads can access sharedCounter
synchronized (ThreadExample.class) {
sharedCounter++;
System.out.println("Worker " + workerId +
" incremented counter to: " + sharedCounter);
}
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
}
}
}
Thread Memory Layout:#
Single Process Memory
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Code Segment (Shared) โ
โ Data Segment (Shared) โ
โ Heap (Shared) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Thread 1 Stack โ
โ Thread 2 Stack โ
โ Thread 3 Stack โ
โ ... (Each thread has own stack) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
layout: default#
Thread Lifecycle and States#
Thread States in Java#
1. NEW#
Thread is created but not yet started
Thread thread = new Thread(() -> System.out.println("Hello"));
// State: NEW
System.out.println("State: " + thread.getState()); // NEW
2. RUNNABLE#
Thread is executing or ready to execute
thread.start(); // Moves to RUNNABLE
// State: RUNNABLE (executing or waiting for CPU time)
3. BLOCKED#
Thread is blocked waiting for a monitor lock
// Thread becomes BLOCKED when trying to enter synchronized block
// that's already locked by another thread
synchronized(someObject) {
// If another thread holds this lock, current thread becomes BLOCKED
}
4. WAITING#
Thread is waiting indefinitely for another thread
// Causes thread to enter WAITING state
Object.wait(); // Wait for notify()
Thread.join(); // Wait for thread completion
LockSupport.park(); // Wait for unpark()
5. TIMED_WAITING#
Thread is waiting for a specified period
// Causes thread to enter TIMED_WAITING state
Thread.sleep(1000); // Wait for 1 second
Object.wait(5000); // Wait max 5 seconds
Thread.join(2000); // Wait max 2 seconds for completion
6. TERMINATED#
Thread has completed execution
// Thread enters TERMINATED state when:
// - run() method completes normally
// - Thread throws an uncaught exception
Thread State Transitions#
stateDiagram-v2
[*] --> NEW : Thread created
NEW --> RUNNABLE : start()
RUNNABLE --> BLOCKED : Monitor lock unavailable
BLOCKED --> RUNNABLE : Monitor lock acquired
RUNNABLE --> WAITING : wait(), join(), park()
WAITING --> RUNNABLE : notify(), thread completion, unpark()
RUNNABLE --> TIMED_WAITING : sleep(), wait(timeout), join(timeout)
TIMED_WAITING --> RUNNABLE : Timeout expires or interrupted
RUNNABLE --> TERMINATED : run() completes or exception
TERMINATED --> [*] : Thread ends
Thread State Example#
public class ThreadStateDemo {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
try {
System.out.println("Worker thread started");
// Simulate some work
Thread.sleep(2000); // TIMED_WAITING
synchronized (ThreadStateDemo.class) {
// Could become BLOCKED if lock not available
System.out.println("In synchronized block");
Thread.sleep(1000);
}
System.out.println("Worker thread finishing");
} catch (InterruptedException e) {
System.out.println("Worker interrupted");
}
});
// Monitor thread states
System.out.println("Before start: " + worker.getState()); // NEW
worker.start();
Thread.sleep(100);
System.out.println("After start: " + worker.getState()); // RUNNABLE
Thread.sleep(1000);
System.out.println("During sleep: " + worker.getState()); // TIMED_WAITING
worker.join(); // Wait for completion
System.out.println("After completion: " + worker.getState()); // TERMINATED
}
}
State Monitoring Utility#
public class ThreadMonitor {
public static void monitorThread(Thread thread, String name) {
Thread monitor = new Thread(() -> {
while (thread.isAlive()) {
System.out.println(name + " state: " + thread.getState());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
break;
}
}
System.out.println(name + " final state: " + thread.getState());
});
monitor.setDaemon(true); // Dies when main thread dies
monitor.start();
}
}
layout: default#
Advantages of Multithreading#
1. Improved Performance#
Parallel Execution#
Multiple tasks can run simultaneously on multi-core systems:
public class PerformanceExample {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
// Sequential approach
sequentialProcessing();
long sequentialTime = System.currentTimeMillis() - startTime;
startTime = System.currentTimeMillis();
// Multithreaded approach
parallelProcessing();
long parallelTime = System.currentTimeMillis() - startTime;
System.out.println("Sequential time: " + sequentialTime + "ms");
System.out.println("Parallel time: " + parallelTime + "ms");
System.out.println("Speedup: " + (double)sequentialTime/parallelTime + "x");
}
private static void sequentialProcessing() {
// Process 4 tasks sequentially
for (int i = 1; i <= 4; i++) {
processTask(i);
}
}
private static void parallelProcessing() {
Thread[] threads = new Thread[4];
// Create and start 4 threads
for (int i = 1; i <= 4; i++) {
final int taskId = i;
threads[i-1] = new Thread(() -> processTask(taskId));
threads[i-1].start();
}
// Wait for all threads to complete
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
private static void processTask(int taskId) {
// Simulate CPU-intensive work
long sum = 0;
for (int i = 0; i < 100000000; i++) {
sum += i;
}
System.out.println("Task " + taskId + " completed, sum: " + sum);
}
}
2. Better Resource Utilization#
CPU Utilization#
public class ResourceUtilizationExample {
public static void main(String[] args) {
int processors = Runtime.getRuntime().availableProcessors();
System.out.println("Available processors: " + processors);
// Create optimal number of threads
Thread[] workers = new Thread[processors];
for (int i = 0; i < processors; i++) {
workers[i] = new Thread(new CPUIntensiveTask(i));
workers[i].start();
}
// Monitor CPU usage
Thread monitor = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("Active threads: " +
Thread.activeCount());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
});
monitor.start();
}
static class CPUIntensiveTask implements Runnable {
private int workerId;
public CPUIntensiveTask(int workerId) {
this.workerId = workerId;
}
@Override
public void run() {
System.out.println("Worker " + workerId + " started on thread: " +
Thread.currentThread().getName());
// CPU-intensive computation
double result = 0;
for (int i = 0; i < 1000000; i++) {
result += Math.sin(i) * Math.cos(i);
}
System.out.println("Worker " + workerId + " completed, result: " + result);
}
}
}
3. Improved User Experience#
Responsive User Interfaces#
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class ResponsiveUIExample {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> createAndShowGUI());
}
private static void createAndShowGUI() {
JFrame frame = new JFrame("Multithreading UI Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(400, 200);
JButton processButton = new JButton("Start Long Process");
JLabel statusLabel = new JLabel("Ready");
JProgressBar progressBar = new JProgressBar(0, 100);
// BAD: This would freeze the UI
/*
processButton.addActionListener(e -> {
for (int i = 0; i <= 100; i++) {
try { Thread.sleep(100); } catch (InterruptedException ex) {}
progressBar.setValue(i);
statusLabel.setText("Processing... " + i + "%");
}
});
*/
// GOOD: Use separate thread for long-running tasks
processButton.addActionListener(e -> {
processButton.setEnabled(false);
statusLabel.setText("Processing started...");
// Background thread for heavy work
Thread worker = new Thread(() -> {
for (int i = 0; i <= 100; i++) {
final int progress = i;
try {
Thread.sleep(100); // Simulate work
} catch (InterruptedException ex) {
return;
}
// Update UI on Event Dispatch Thread
SwingUtilities.invokeLater(() -> {
progressBar.setValue(progress);
statusLabel.setText("Processing... " + progress + "%");
if (progress == 100) {
statusLabel.setText("Completed!");
processButton.setEnabled(true);
}
});
}
});
worker.start();
});
JPanel panel = new JPanel();
panel.add(processButton);
panel.add(statusLabel);
panel.add(progressBar);
frame.add(panel);
frame.setVisible(true);
}
}
4. Better I/O Handling#
Concurrent I/O Operations#
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
public class ConcurrentIOExample {
public static void main(String[] args) {
// Download multiple files concurrently
String[] urls = {
"http://example.com/file1.txt",
"http://example.com/file2.txt",
"http://example.com/file3.txt"
};
ExecutorService executor = Executors.newFixedThreadPool(3);
CompletionService<String> completionService =
new ExecutorCompletionService<>(executor);
// Submit download tasks
for (String url : urls) {
completionService.submit(new DownloadTask(url));
}
// Process results as they complete
for (int i = 0; i < urls.length; i++) {
try {
Future<String> result = completionService.take();
System.out.println("Download completed: " + result.get());
} catch (Exception e) {
System.out.println("Download failed: " + e.getMessage());
}
}
executor.shutdown();
}
static class DownloadTask implements Callable<String> {
private String url;
public DownloadTask(String url) {
this.url = url;
}
@Override
public String call() throws Exception {
System.out.println("Starting download: " + url +
" on thread " + Thread.currentThread().getName());
// Simulate network delay
Thread.sleep((int)(Math.random() * 3000 + 1000));
// Simulate download
return url + " (Size: " + (int)(Math.random() * 1000 + 100) + "KB)";
}
}
}
layout: default#
Concurrency vs Parallelism#
Concurrency#
Concurrency is about dealing with multiple tasks at once - tasks may not run simultaneously but appear to do so through time-slicing.
Characteristics:#
- Single-core systems: Tasks are interleaved
- Time-slicing: CPU switches between tasks rapidly
- Appears simultaneous: Users perceive tasks as running together
- Task management: Focus on coordinating multiple tasks
Concurrency Example:#
public class ConcurrencyExample {
public static void main(String[] args) {
System.out.println("CPU cores: " +
Runtime.getRuntime().availableProcessors());
// Create more threads than CPU cores
for (int i = 1; i <= 10; i++) {
Thread worker = new Thread(new ConcurrentTask(i));
worker.start();
}
}
static class ConcurrentTask implements Runnable {
private int taskId;
public ConcurrentTask(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Task " + taskId + " - Step " + i +
" on thread " + Thread.currentThread().getName());
// Simulate work and allow other threads to run
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
}
}
/* Output might look like (on single-core system):
Task 1 - Step 1 on thread Thread-1
Task 2 - Step 1 on thread Thread-2
Task 3 - Step 1 on thread Thread-3
Task 1 - Step 2 on thread Thread-1
Task 4 - Step 1 on thread Thread-4
... (tasks interleaved by OS scheduler)
*/
Concurrency Visualization:#
Single Core Timeline:
Time: 0---1---2---3---4---5---6---7---8---9---10
Task A: [==] [==] [==] [==]
Task B: [==] [==] [==] [==]
Task C: [==] [==]
Parallelism#
Parallelism is about actually doing multiple tasks simultaneously - requires multiple cores/processors.
Characteristics:#
- Multi-core systems: Tasks run simultaneously on different cores
- True simultaneity: Tasks execute at exactly the same time
- Hardware dependent: Requires multiple processing units
- Performance focused: Aimed at faster execution
Parallelism Example:#
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
public class ParallelismExample {
public static void main(String[] args) {
int[] array = new int[1000000];
// Initialize array
for (int i = 0; i < array.length; i++) {
array[i] = i + 1;
}
// Sequential sum
long startTime = System.nanoTime();
long sequentialSum = sequentialSum(array);
long sequentialTime = System.nanoTime() - startTime;
// Parallel sum using Fork-Join framework
startTime = System.nanoTime();
ForkJoinPool pool = new ForkJoinPool();
long parallelSum = pool.invoke(new ParallelSumTask(array, 0, array.length));
long parallelTime = System.nanoTime() - startTime;
System.out.println("Sequential sum: " + sequentialSum +
" (Time: " + sequentialTime/1000000 + "ms)");
System.out.println("Parallel sum: " + parallelSum +
" (Time: " + parallelTime/1000000 + "ms)");
System.out.println("Speedup: " +
(double)sequentialTime/parallelTime + "x");
pool.shutdown();
}
private static long sequentialSum(int[] array) {
long sum = 0;
for (int value : array) {
sum += value;
}
return sum;
}
static class ParallelSumTask extends RecursiveTask<Long> {
private final int[] array;
private final int start;
private final int end;
private static final int THRESHOLD = 10000;
public ParallelSumTask(int[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if (end - start <= THRESHOLD) {
// Small enough - compute directly
long sum = 0;
for (int i = start; i < end; i++) {
sum += array[i];
}
return sum;
} else {
// Split into subtasks
int middle = (start + end) / 2;
ParallelSumTask leftTask = new ParallelSumTask(array, start, middle);
ParallelSumTask rightTask = new ParallelSumTask(array, middle, end);
// Fork right task (runs on different core)
rightTask.fork();
// Compute left task on current core
long leftResult = leftTask.compute();
// Join right task result
long rightResult = rightTask.join();
return leftResult + rightResult;
}
}
}
}
Parallelism Visualization:#
Multi-Core Timeline:
Time: 0---1---2---3---4---5
Core 1: [Task A - Part 1 ]
Core 2: [Task A - Part 2 ]
Core 3: [Task B - Complete ]
Core 4: [Task C - Complete ]
layout: default#
Multithreading Use Cases#
1. Web Server Applications#
Handling Multiple Client Requests#
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
public class MultiThreadedWebServer {
private final int port;
private final ExecutorService threadPool;
private volatile boolean running = false;
public MultiThreadedWebServer(int port) {
this.port = port;
this.threadPool = Executors.newFixedThreadPool(50); // Handle 50 concurrent clients
}
public void start() throws IOException {
running = true;
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("Server started on port " + port);
while (running) {
// Accept client connection
Socket clientSocket = serverSocket.accept();
// Handle each client in separate thread
threadPool.submit(new ClientHandler(clientSocket));
}
}
}
private static class ClientHandler implements Runnable {
private final Socket clientSocket;
public ClientHandler(Socket clientSocket) {
this.clientSocket = clientSocket;
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println("Handling client on thread: " + threadName);
try (BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(
clientSocket.getOutputStream(), true)) {
// Read client request
String request = in.readLine();
System.out.println("Request from client: " + request);
// Simulate processing time
Thread.sleep(2000);
// Send response
out.println("HTTP/1.1 200 OK");
out.println("Content-Type: text/html");
out.println("");
out.println("<html><body>");
out.println("<h1>Hello from thread: " + threadName + "</h1>");
out.println("<p>Your request: " + request + "</p>");
out.println("</body></html>");
System.out.println("Response sent to client on thread: " + threadName);
} catch (IOException | InterruptedException e) {
System.err.println("Error handling client: " + e.getMessage());
} finally {
try {
clientSocket.close();
} catch (IOException e) {
System.err.println("Error closing client socket: " + e.getMessage());
}
}
}
}
public void stop() {
running = false;
threadPool.shutdown();
try {
if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
threadPool.shutdownNow();
}
} catch (InterruptedException e) {
threadPool.shutdownNow();
}
}
public static void main(String[] args) {
MultiThreadedWebServer server = new MultiThreadedWebServer(8080);
try {
server.start();
} catch (IOException e) {
System.err.println("Server error: " + e.getMessage());
}
}
}
2. File Processing Applications#
Concurrent File Processing#
import java.io.*;
import java.nio.file.*;
import java.util.concurrent.*;
import java.util.stream.Stream;
public class ConcurrentFileProcessor {
private final ExecutorService executor;
private final String inputDirectory;
private final String outputDirectory;
public ConcurrentFileProcessor(String inputDir, String outputDir) {
this.inputDirectory = inputDir;
this.outputDirectory = outputDir;
this.executor = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors());
}
public void processAllFiles() {
try {
// Create output directory if it doesn't exist
Files.createDirectories(Paths.get(outputDirectory));
// Get all files to process
try (Stream<Path> files = Files.walk(Paths.get(inputDirectory))) {
CompletionService<String> completionService =
new ExecutorCompletionService<>(executor);
// Submit processing tasks for each file
long fileCount = files
.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".txt"))
.mapToLong(path -> {
completionService.submit(new FileProcessingTask(path));
return 1;
}).sum();
System.out.println("Processing " + fileCount + " files concurrently...");
// Collect results as they complete
for (int i = 0; i < fileCount; i++) {
try {
Future<String> result = completionService.take();
System.out.println("Completed: " + result.get());
} catch (Exception e) {
System.err.println("Task failed: " + e.getMessage());
}
}
}
} catch (IOException e) {
System.err.println("Error accessing files: " + e.getMessage());
} finally {
executor.shutdown();
}
}
private class FileProcessingTask implements Callable<String> {
private final Path inputFile;
public FileProcessingTask(Path inputFile) {
this.inputFile = inputFile;
}
@Override
public String call() throws Exception {
String threadName = Thread.currentThread().getName();
String fileName = inputFile.getFileName().toString();
System.out.println("Processing " + fileName + " on " + threadName);
// Read input file
String content = Files.readString(inputFile);
// Simulate processing (convert to uppercase, count words, etc.)
String processedContent = processContent(content);
// Write to output file
Path outputFile = Paths.get(outputDirectory, "processed_" + fileName);
Files.writeString(outputFile, processedContent);
return fileName + " -> " + outputFile.getFileName() +
" (processed by " + threadName + ")";
}
private String processContent(String content) throws InterruptedException {
// Simulate CPU-intensive processing
Thread.sleep(1000 + (int)(Math.random() * 2000));
// Process content
String[] words = content.split("\\s+");
StringBuilder result = new StringBuilder();
result.append("PROCESSED FILE\n");
result.append("=============\n");
result.append("Word count: ").append(words.length).append("\n");
result.append("Character count: ").append(content.length()).append("\n");
result.append("Processed by thread: ").append(Thread.currentThread().getName()).append("\n\n");
result.append("UPPERCASE CONTENT:\n");
result.append(content.toUpperCase());
return result.toString();
}
}
public static void main(String[] args) {
ConcurrentFileProcessor processor = new ConcurrentFileProcessor(
"input_files", "output_files");
long startTime = System.currentTimeMillis();
processor.processAllFiles();
long endTime = System.currentTimeMillis();
System.out.println("Total processing time: " + (endTime - startTime) + "ms");
}
}
layout: default#
Thread Scheduling and Context Switching#
Thread Scheduling#
What is Thread Scheduling?#
Thread scheduling is the process by which the operating system decides which thread gets to run on the CPU at any given time.
Types of Scheduling:#
1. Preemptive Scheduling#
- OS can interrupt running threads
- Time-slicing: each thread gets CPU for fixed time
- Higher priority threads can preempt lower priority ones
2. Cooperative Scheduling#
- Threads voluntarily yield CPU control
- Thread runs until it blocks or explicitly yields
- Less common in modern systems
Java Thread Priorities#
public class ThreadPriorityExample {
public static void main(String[] args) {
Thread lowPriorityThread = new Thread(new CountingTask("Low Priority"));
Thread normalPriorityThread = new Thread(new CountingTask("Normal Priority"));
Thread highPriorityThread = new Thread(new CountingTask("High Priority"));
// Set thread priorities
lowPriorityThread.setPriority(Thread.MIN_PRIORITY); // 1
normalPriorityThread.setPriority(Thread.NORM_PRIORITY); // 5
highPriorityThread.setPriority(Thread.MAX_PRIORITY); // 10
System.out.println("Thread priorities:");
System.out.println("Low: " + lowPriorityThread.getPriority());
System.out.println("Normal: " + normalPriorityThread.getPriority());
System.out.println("High: " + highPriorityThread.getPriority());
// Start threads
lowPriorityThread.start();
normalPriorityThread.start();
highPriorityThread.start();
// Let them run for 3 seconds
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// Stop all threads
CountingTask.stop();
}
static class CountingTask implements Runnable {
private String name;
private static volatile boolean running = true;
public CountingTask(String name) {
this.name = name;
}
@Override
public void run() {
long count = 0;
while (running) {
count++;
// Print progress occasionally
if (count % 1000000 == 0) {
System.out.println(name + " thread count: " + count +
" (Priority: " + Thread.currentThread().getPriority() + ")");
}
}
System.out.println(name + " final count: " + count);
}
public static void stop() {
running = false;
}
}
}
Context Switching#
What is Context Switching?#
Context switching is the process of saving and restoring thread state when the CPU switches from executing one thread to another.
Context Switch Process:#
Save Current Thread State:
- Program counter (PC)
- CPU registers
- Stack pointer
- Thread-specific data
Load New Thread State:
- Restore program counter
- Restore CPU registers
- Switch to new thread’s stack
- Update memory mappings if needed
Resume Execution:
- Continue from where new thread left off
Context Switch Overhead#
public class ContextSwitchDemo {
private static final int ITERATIONS = 1000000;
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
// Demonstrate context switching overhead
// Test 1: Single thread (no context switching)
long startTime = System.nanoTime();
singleThreadWork();
long singleThreadTime = System.nanoTime() - startTime;
// Test 2: Two threads with frequent context switches
startTime = System.nanoTime();
twoThreadWork();
long twoThreadTime = System.nanoTime() - startTime;
System.out.println("Single thread time: " + singleThreadTime/1000000 + "ms");
System.out.println("Two thread time: " + twoThreadTime/1000000 + "ms");
System.out.println("Overhead factor: " + (double)twoThreadTime/singleThreadTime);
}
private static void singleThreadWork() {
int count = 0;
for (int i = 0; i < ITERATIONS; i++) {
count = (count + 1) % 1000;
}
System.out.println("Single thread final count: " + count);
}
private static void twoThreadWork() throws InterruptedException {
Thread thread1 = new Thread(new PingPongTask("Thread1"));
Thread thread2 = new Thread(new PingPongTask("Thread2"));
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
static class PingPongTask implements Runnable {
private String name;
public PingPongTask(String name) {
this.name = name;
}
@Override
public void run() {
int count = 0;
for (int i = 0; i < ITERATIONS / 2; i++) {
// Force context switches by yielding
if (i % 100 == 0) {
Thread.yield();
}
count = (count + 1) % 1000;
}
System.out.println(name + " final count: " + count);
}
}
}
Minimizing Context Switches#
public class ContextSwitchOptimization {
public static void main(String[] args) throws InterruptedException {
System.out.println("Demonstrating context switch optimization...");
// Bad: Frequent synchronization causing context switches
long startTime = System.currentTimeMillis();
frequentSynchronization();
long badTime = System.currentTimeMillis() - startTime;
// Good: Batch operations to reduce context switches
startTime = System.currentTimeMillis();
batchedOperations();
long goodTime = System.currentTimeMillis() - startTime;
System.out.println("Frequent sync time: " + badTime + "ms");
System.out.println("Batched operations time: " + goodTime + "ms");
System.out.println("Improvement: " + (double)badTime/goodTime + "x");
}
private static void frequentSynchronization() throws InterruptedException {
final Object lock = new Object();
final int[] counter = {0};
Thread[] threads = new Thread[4];
for (int i = 0; i < 4; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
synchronized (lock) {
counter[0]++; // Frequent lock contention
}
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("Frequent sync counter: " + counter[0]);
}
private static void batchedOperations() throws InterruptedException {
final Object lock = new Object();
final int[] counter = {0};
Thread[] threads = new Thread[4];
for (int i = 0; i < 4; i++) {
threads[i] = new Thread(() -> {
int localCount = 0;
// Batch work locally
for (int j = 0; j < 10000; j++) {
localCount++;
}
// Single synchronization per thread
synchronized (lock) {
counter[0] += localCount;
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("Batched operations counter: " + counter[0]);
}
}
layout: default#
Challenges and Considerations#
1. Race Conditions#
What is a Race Condition?#
A race condition occurs when multiple threads access shared data concurrently, and the final result depends on the timing of their execution.
public class RaceConditionExample {
private static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
// Create 10 threads that increment counter
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter++; // Race condition: non-atomic operation
}
});
}
// Start all threads
for (Thread thread : threads) {
thread.start();
}
// Wait for all threads to complete
for (Thread thread : threads) {
thread.join();
}
System.out.println("Expected result: 10000");
System.out.println("Actual result: " + counter);
System.out.println("Data lost due to race condition: " + (10000 - counter));
}
}
/* Possible output (varies each run):
Expected result: 10000
Actual result: 7834
Data lost due to race condition: 2166
*/
Why Race Conditions Occur:#
// The counter++ operation is actually three steps:
// 1. Read current value of counter
// 2. Add 1 to the value
// 3. Write new value back to counter
// Thread execution timeline:
// Time | Thread A | Thread B | Counter Value
// 1 | Read(5) | | 5
// 2 | | Read(5) | 5
// 3 | Add 1(6) | | 5
// 4 | | Add 1(6) | 5
// 5 | Write(6) | | 6
// 6 | | Write(6) | 6 (Should be 7!)
// Solution: Synchronization
public class FixedRaceCondition {
private static int counter = 0;
private static final Object lock = new Object();
public static void increment() {
synchronized (lock) {
counter++; // Now atomic
}
}
}
2. Deadlocks#
What is a Deadlock?#
A deadlock occurs when two or more threads are blocked forever, waiting for each other to release resources.
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock1...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 1: Waiting for lock2...");
synchronized (lock2) { // Will wait forever
System.out.println("Thread 1: Got both locks!");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock2...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 2: Waiting for lock1...");
synchronized (lock1) { // Will wait forever
System.out.println("Thread 2: Got both locks!");
}
}
});
thread1.start();
thread2.start();
// Threads will deadlock - program hangs
}
}
3. Thread Safety Issues#
Non-thread-safe Collections#
import java.util.*;
import java.util.concurrent.*;
public class ThreadSafetyExample {
public static void main(String[] args) throws InterruptedException {
// Unsafe: ArrayList is not thread-safe
demonstrateUnsafeCollection();
// Safe: Using thread-safe alternatives
demonstrateSafeCollection();
}
private static void demonstrateUnsafeCollection() throws InterruptedException {
List<Integer> unsafeList = new ArrayList<>();
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
final int threadId = i;
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
unsafeList.add(threadId * 1000 + j);
}
});
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("Unsafe list size: " + unsafeList.size() +
" (expected: 10000)");
// Size might be less than 10000 due to race conditions
}
private static void demonstrateSafeCollection() throws InterruptedException {
// Option 1: Synchronized collection
List<Integer> synchronizedList = Collections.synchronizedList(new ArrayList<>());
// Option 2: Concurrent collection
List<Integer> concurrentList = new CopyOnWriteArrayList<>();
// Option 3: Manual synchronization
List<Integer> manualSyncList = new ArrayList<>();
Object lock = new Object();
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
final int threadId = i;
threads[i] = new Thread(() -> {
for (int j = 0; j < 100; j++) {
// Safe operations
synchronizedList.add(threadId * 100 + j);
concurrentList.add(threadId * 100 + j);
synchronized (lock) {
manualSyncList.add(threadId * 100 + j);
}
}
});
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("Synchronized list size: " + synchronizedList.size());
System.out.println("Concurrent list size: " + concurrentList.size());
System.out.println("Manual sync list size: " + manualSyncList.size());
}
}
4. Performance Overhead#
Thread Creation Overhead#
public class ThreadOverheadExample {
public static void main(String[] args) {
// Compare thread creation vs thread pool
// Test 1: Creating new threads each time (expensive)
long startTime = System.currentTimeMillis();
createNewThreads(1000);
long newThreadTime = System.currentTimeMillis() - startTime;
// Test 2: Using thread pool (efficient)
startTime = System.currentTimeMillis();
useThreadPool(1000);
long threadPoolTime = System.currentTimeMillis() - startTime;
System.out.println("New threads time: " + newThreadTime + "ms");
System.out.println("Thread pool time: " + threadPoolTime + "ms");
System.out.println("Thread pool is " + (double)newThreadTime/threadPoolTime +
"x faster");
}
private static void createNewThreads(int taskCount) {
for (int i = 0; i < taskCount; i++) {
Thread thread = new Thread(() -> {
// Simulate light work
int sum = 0;
for (int j = 0; j < 1000; j++) {
sum += j;
}
});
thread.start();
try {
thread.join(); // Wait for completion
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
private static void useThreadPool(int taskCount) {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < taskCount; i++) {
executor.submit(() -> {
// Simulate same light work
int sum = 0;
for (int j = 0; j < 1000; j++) {
sum += j;
}
});
}
executor.shutdown();
try {
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
layout: default#
Hands-on Exercise 1: Producer-Consumer Simulation#
Task: Build Producer-Consumer System#
Create a multithreaded system where producers generate data and consumers process it:
// TODO: Implement shared buffer
public class SharedBuffer {
private Queue<Integer> buffer;
private int capacity;
public SharedBuffer(int capacity) {
// Initialize buffer with capacity limit
}
// TODO: Thread-safe method to add item
public synchronized void produce(int item) throws InterruptedException {
// Wait if buffer is full
// Add item to buffer
// Notify waiting consumers
}
// TODO: Thread-safe method to remove item
public synchronized int consume() throws InterruptedException {
// Wait if buffer is empty
// Remove and return item from buffer
// Notify waiting producers
return 0;
}
public synchronized int size() {
return buffer.size();
}
public synchronized boolean isEmpty() {
return buffer.isEmpty();
}
public synchronized boolean isFull() {
return buffer.size() >= capacity;
}
}
// TODO: Implement Producer thread
public class Producer implements Runnable {
private SharedBuffer buffer;
private int id;
private int itemsToProducer;
public Producer(SharedBuffer buffer, int id, int itemsToProducer) {
// Initialize producer
}
@Override
public void run() {
// Produce items and add to buffer
// Handle interruptions properly
// Log production activities
}
}
// TODO: Implement Consumer thread
public class Consumer implements Runnable {
private SharedBuffer buffer;
private int id;
private int itemsToConsume;
public Consumer(SharedBuffer buffer, int id, int itemsToConsume) {
// Initialize consumer
}
@Override
public void run() {
// Consume items from buffer
// Handle interruptions properly
// Log consumption activities
}
}
// TODO: Implement main simulation
public class ProducerConsumerSimulation {
public static void main(String[] args) {
// Create shared buffer
// Create multiple producer threads
// Create multiple consumer threads
// Start all threads
// Monitor and display statistics
// Handle graceful shutdown
}
}
Requirements:
- Handle buffer full/empty conditions properly
- Implement proper thread synchronization
- Add comprehensive logging and statistics
- Handle thread interruption gracefully
- Show real-time statistics (items produced/consumed, buffer size)
Solution Framework#
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
public class SharedBuffer {
private final Queue<Integer> buffer = new LinkedList<>();
private final int capacity;
private final AtomicInteger totalProduced = new AtomicInteger(0);
private final AtomicInteger totalConsumed = new AtomicInteger(0);
public SharedBuffer(int capacity) {
this.capacity = capacity;
}
public synchronized void produce(int item) throws InterruptedException {
// Wait while buffer is full
while (buffer.size() >= capacity) {
System.out.println("Buffer full, producer waiting...");
wait(); // Release lock and wait
}
buffer.offer(item);
totalProduced.incrementAndGet();
System.out.println("Produced: " + item + ", Buffer size: " + buffer.size());
notifyAll(); // Notify waiting consumers
}
public synchronized int consume() throws InterruptedException {
// Wait while buffer is empty
while (buffer.isEmpty()) {
System.out.println("Buffer empty, consumer waiting...");
wait(); // Release lock and wait
}
int item = buffer.poll();
totalConsumed.incrementAndGet();
System.out.println("Consumed: " + item + ", Buffer size: " + buffer.size());
notifyAll(); // Notify waiting producers
return item;
}
public synchronized int size() { return buffer.size(); }
public int getTotalProduced() { return totalProduced.get(); }
public int getTotalConsumed() { return totalConsumed.get(); }
}
public class Producer implements Runnable {
private final SharedBuffer buffer;
private final int id;
private final int itemsToProducer;
private final Random random = new Random();
public Producer(SharedBuffer buffer, int id, int itemsToProducer) {
this.buffer = buffer;
this.id = id;
this.itemsToProducer = itemsToProducer;
}
@Override
public void run() {
try {
for (int i = 1; i <= itemsToProducer; i++) {
int item = id * 1000 + i; // Unique item ID
buffer.produce(item);
// Random production delay
Thread.sleep(random.nextInt(100) + 50);
}
System.out.println("Producer " + id + " finished producing " +
itemsToProducer + " items");
} catch (InterruptedException e) {
System.out.println("Producer " + id + " interrupted");
Thread.currentThread().interrupt();
}
}
}
public class Consumer implements Runnable {
private final SharedBuffer buffer;
private final int id;
private final int itemsToConsume;
private final Random random = new Random();
public Consumer(SharedBuffer buffer, int id, int itemsToConsume) {
this.buffer = buffer;
this.id = id;
this.itemsToConsume = itemsToConsume;
}
@Override
public void run() {
try {
for (int i = 1; i <= itemsToConsume; i++) {
int item = buffer.consume();
// Simulate processing time
Thread.sleep(random.nextInt(150) + 75);
}
System.out.println("Consumer " + id + " finished consuming " +
itemsToConsume + " items");
} catch (InterruptedException e) {
System.out.println("Consumer " + id + " interrupted");
Thread.currentThread().interrupt();
}
}
}
public class ProducerConsumerSimulation {
public static void main(String[] args) throws InterruptedException {
SharedBuffer buffer = new SharedBuffer(10); // Buffer capacity: 10
// Create producer threads
Thread[] producers = new Thread[3];
for (int i = 0; i < 3; i++) {
producers[i] = new Thread(new Producer(buffer, i + 1, 20));
producers[i].setName("Producer-" + (i + 1));
}
// Create consumer threads
Thread[] consumers = new Thread[2];
for (int i = 0; i < 2; i++) {
consumers[i] = new Thread(new Consumer(buffer, i + 1, 30));
consumers[i].setName("Consumer-" + (i + 1));
}
// Start statistics monitor
Thread monitor = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(2000);
System.out.println("\n=== STATISTICS ===");
System.out.println("Buffer size: " + buffer.size());
System.out.println("Total produced: " + buffer.getTotalProduced());
System.out.println("Total consumed: " + buffer.getTotalConsumed());
System.out.println("==================\n");
} catch (InterruptedException e) {
break;
}
}
});
monitor.setDaemon(true);
monitor.start();
// Start all threads
for (Thread producer : producers) {
producer.start();
}
for (Thread consumer : consumers) {
consumer.start();
}
// Wait for completion
for (Thread producer : producers) {
producer.join();
}
for (Thread consumer : consumers) {
consumer.join();
}
// Final statistics
System.out.println("\n=== FINAL STATISTICS ===");
System.out.println("Total produced: " + buffer.getTotalProduced());
System.out.println("Total consumed: " + buffer.getTotalConsumed());
System.out.println("Remaining in buffer: " + buffer.size());
}
}
layout: default#
Hands-on Exercise 2: Download Manager#
Task: Build Multithreaded Download Manager#
Create a download manager that can download multiple files concurrently:
// TODO: Implement Download task
public class DownloadTask implements Callable<DownloadResult> {
private String url;
private String fileName;
private String downloadDirectory;
public DownloadTask(String url, String fileName, String downloadDirectory) {
// Initialize download task
}
@Override
public DownloadResult call() throws Exception {
// Simulate file download
// Show download progress
// Handle download errors
// Return download result
return null;
}
private void simulateDownload(String fileName, long fileSize) {
// Simulate downloading with progress updates
}
}
// TODO: Implement Download result
public class DownloadResult {
private String fileName;
private boolean success;
private long fileSize;
private long downloadTime;
private String errorMessage;
// Constructor, getters, setters
}
// TODO: Implement Download manager
public class DownloadManager {
private ExecutorService executor;
private CompletionService<DownloadResult> completionService;
public DownloadManager(int maxConcurrentDownloads) {
// Initialize thread pool and completion service
}
public void addDownload(String url, String fileName) {
// Add download task to queue
}
public void startDownloads() {
// Process all downloads concurrently
// Show overall progress
// Handle completed downloads
}
public void shutdown() {
// Graceful shutdown of download manager
}
}
// TODO: Implement main application
public class DownloadManagerApp {
public static void main(String[] args) {
// Create download manager
// Add multiple download tasks
// Start downloads
// Show progress and statistics
// Handle user input (pause, cancel, etc.)
}
}
Requirements:
- Support concurrent downloads with configurable thread count
- Show individual download progress and overall progress
- Handle download failures gracefully
- Provide download statistics (speed, time, success rate)
- Support pause/resume functionality
- Implement proper resource cleanup
Solution Implementation#
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.*;
public class DownloadTask implements Callable<DownloadResult> {
private final String url;
private final String fileName;
private final String downloadDirectory;
private static final Random random = new Random();
public DownloadTask(String url, String fileName, String downloadDirectory) {
this.url = url;
this.fileName = fileName;
this.downloadDirectory = downloadDirectory;
}
@Override
public DownloadResult call() throws Exception {
long startTime = System.currentTimeMillis();
String threadName = Thread.currentThread().getName();
try {
System.out.println("Starting download: " + fileName + " on " + threadName);
// Simulate random file size (1MB to 100MB)
long fileSize = (long)(Math.random() * 100_000_000 + 1_000_000);
// Simulate potential download failure (10% chance)
if (random.nextDouble() < 0.1) {
throw new Exception("Network error: Connection timeout");
}
// Simulate download with progress
simulateDownload(fileName, fileSize);
long downloadTime = System.currentTimeMillis() - startTime;
System.out.println("Completed download: " + fileName +
" (" + formatFileSize(fileSize) + " in " + downloadTime + "ms)");
return new DownloadResult(fileName, true, fileSize, downloadTime, null);
} catch (Exception e) {
long downloadTime = System.currentTimeMillis() - startTime;
System.err.println("Failed download: " + fileName + " - " + e.getMessage());
return new DownloadResult(fileName, false, 0, downloadTime, e.getMessage());
}
}
private void simulateDownload(String fileName, long fileSize) throws InterruptedException {
long downloaded = 0;
long chunkSize = fileSize / 20; // 20 progress updates
while (downloaded < fileSize) {
// Simulate network delay
Thread.sleep(100 + random.nextInt(200));
downloaded = Math.min(downloaded + chunkSize, fileSize);
int progress = (int)((downloaded * 100) / fileSize);
System.out.printf(" %s: %d%% (%s/%s)%n",
fileName, progress,
formatFileSize(downloaded), formatFileSize(fileSize));
}
}
private String formatFileSize(long bytes) {
if (bytes >= 1_000_000) {
return String.format("%.1fMB", bytes / 1_000_000.0);
} else if (bytes >= 1_000) {
return String.format("%.1fKB", bytes / 1_000.0);
} else {
return bytes + "B";
}
}
}
public class DownloadResult {
private final String fileName;
private final boolean success;
private final long fileSize;
private final long downloadTime;
private final String errorMessage;
public DownloadResult(String fileName, boolean success, long fileSize,
long downloadTime, String errorMessage) {
this.fileName = fileName;
this.success = success;
this.fileSize = fileSize;
this.downloadTime = downloadTime;
this.errorMessage = errorMessage;
}
// Getters
public String getFileName() { return fileName; }
public boolean isSuccess() { return success; }
public long getFileSize() { return fileSize; }
public long getDownloadTime() { return downloadTime; }
public String getErrorMessage() { return errorMessage; }
public double getDownloadSpeed() {
if (downloadTime == 0 || !success) return 0;
return (fileSize / 1024.0 / 1024.0) / (downloadTime / 1000.0); // MB/s
}
}
public class DownloadManager {
private final ExecutorService executor;
private final CompletionService<DownloadResult> completionService;
private final List<DownloadTask> downloadTasks;
private final AtomicInteger completedDownloads = new AtomicInteger(0);
private final AtomicInteger successfulDownloads = new AtomicInteger(0);
private final AtomicLong totalBytesDownloaded = new AtomicLong(0);
public DownloadManager(int maxConcurrentDownloads) {
this.executor = Executors.newFixedThreadPool(maxConcurrentDownloads);
this.completionService = new ExecutorCompletionService<>(executor);
this.downloadTasks = new ArrayList<>();
}
public void addDownload(String url, String fileName) {
downloadTasks.add(new DownloadTask(url, fileName, "downloads/"));
System.out.println("Added to download queue: " + fileName);
}
public void startDownloads() {
if (downloadTasks.isEmpty()) {
System.out.println("No downloads in queue");
return;
}
System.out.println("Starting " + downloadTasks.size() + " downloads...");
// Submit all download tasks
for (DownloadTask task : downloadTasks) {
completionService.submit(task);
}
// Start progress monitor
Thread progressMonitor = createProgressMonitor();
progressMonitor.start();
// Process completed downloads
List<DownloadResult> results = new ArrayList<>();
for (int i = 0; i < downloadTasks.size(); i++) {
try {
Future<DownloadResult> future = completionService.take();
DownloadResult result = future.get();
results.add(result);
completedDownloads.incrementAndGet();
if (result.isSuccess()) {
successfulDownloads.incrementAndGet();
totalBytesDownloaded.addAndGet(result.getFileSize());
}
} catch (InterruptedException | ExecutionException e) {
System.err.println("Error processing download result: " + e.getMessage());
}
}
// Stop progress monitor
progressMonitor.interrupt();
// Display final results
displayFinalResults(results);
}
private Thread createProgressMonitor() {
return new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(3000); // Update every 3 seconds
int total = downloadTasks.size();
int completed = completedDownloads.get();
int successful = successfulDownloads.get();
System.out.println("\n=== DOWNLOAD PROGRESS ===");
System.out.printf("Progress: %d/%d completed (%d successful)%n",
completed, total, successful);
System.out.printf("Total downloaded: %s%n",
formatFileSize(totalBytesDownloaded.get()));
System.out.printf("Active downloads: %d%n",
total - completed);
System.out.println("========================\n");
} catch (InterruptedException e) {
break;
}
}
});
}
private void displayFinalResults(List<DownloadResult> results) {
System.out.println("\n=== DOWNLOAD COMPLETE ===");
System.out.println("Total downloads: " + results.size());
System.out.println("Successful: " + successfulDownloads.get());
System.out.println("Failed: " + (results.size() - successfulDownloads.get()));
System.out.println("Total bytes downloaded: " + formatFileSize(totalBytesDownloaded.get()));
// Calculate average download speed
double totalSpeed = results.stream()
.filter(DownloadResult::isSuccess)
.mapToDouble(DownloadResult::getDownloadSpeed)
.sum();
if (successfulDownloads.get() > 0) {
double avgSpeed = totalSpeed / successfulDownloads.get();
System.out.printf("Average download speed: %.2f MB/s%n", avgSpeed);
}
// Show failed downloads
results.stream()
.filter(r -> !r.isSuccess())
.forEach(r -> System.out.println("Failed: " + r.getFileName() +
" - " + r.getErrorMessage()));
System.out.println("========================");
}
private String formatFileSize(long bytes) {
if (bytes >= 1_000_000_000) {
return String.format("%.2fGB", bytes / 1_000_000_000.0);
} else if (bytes >= 1_000_000) {
return String.format("%.2fMB", bytes / 1_000_000.0);
} else if (bytes >= 1_000) {
return String.format("%.2fKB", bytes / 1_000.0);
} else {
return bytes + "B";
}
}
public void shutdown() {
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("Download manager did not terminate");
}
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
public class DownloadManagerApp {
public static void main(String[] args) {
DownloadManager manager = new DownloadManager(5); // Max 5 concurrent downloads
// Add sample downloads
manager.addDownload("http://example.com/file1.zip", "Software_Package.zip");
manager.addDownload("http://example.com/movie.mp4", "Action_Movie.mp4");
manager.addDownload("http://example.com/document.pdf", "Manual.pdf");
manager.addDownload("http://example.com/music.mp3", "Song.mp3");
manager.addDownload("http://example.com/presentation.pptx", "Slides.pptx");
manager.addDownload("http://example.com/data.csv", "Data_Export.csv");
manager.addDownload("http://example.com/image.jpg", "Photo.jpg");
manager.addDownload("http://example.com/archive.tar.gz", "Backup.tar.gz");
// Start downloads
long startTime = System.currentTimeMillis();
manager.startDownloads();
long totalTime = System.currentTimeMillis() - startTime;
System.out.println("\nTotal execution time: " + totalTime + "ms");
// Cleanup
manager.shutdown();
}
}
layout: default#
Summary and Key Takeaways#
What We Learned#
- ๐งต Thread Fundamentals: Basic concepts of threads and multithreading
- ๐ Process vs Thread: Key differences and when to use each
- ๐ Thread Lifecycle: Six states and transitions between them
- โก Multithreading Benefits: Performance, resource utilization, user experience
- ๐ฏ Concurrency vs Parallelism: Understanding the distinction and applications
- ๐ง Thread Scheduling: How OS manages thread execution
- ๐๏ธ Real-world Applications: Web servers, file processing, download managers
Multithreading Advantages#
Performance Benefits#
- Parallel Execution: Multiple tasks run simultaneously
- Better CPU Utilization: Use all available processor cores
- Faster Response Times: Background processing doesn’t block main thread
- Improved Throughput: Handle more requests per unit time
User Experience Benefits#
- Responsive Interfaces: UI remains interactive during long operations
- Background Processing: Heavy tasks don’t freeze applications
- Real-time Updates: Progress indicators and live data feeds
- Multitasking: Users can perform multiple operations simultaneously
Resource Efficiency#
- Shared Memory: Threads share process memory space
- Lower Overhead: Thread creation cheaper than process creation
- Better I/O Handling: Concurrent file/network operations
- Optimal Resource Usage: Threads can share system resources
Key Concepts Recap#
- Thread States: NEW โ RUNNABLE โ BLOCKED/WAITING/TIMED_WAITING โ TERMINATED
- Race Conditions: Multiple threads accessing shared data unsafely
- Context Switching: OS switching between threads (has overhead)
- Thread Safety: Ensuring correct behavior in multithreaded environment
- Synchronization: Coordinating thread access to shared resources
- Deadlocks: Threads waiting for each other indefinitely
- Thread Pools: Reusing threads for better performance
Challenges and Solutions#
Common Challenges#
- Race Conditions: Use synchronization mechanisms
- Deadlocks: Careful lock ordering and timeout strategies
- Thread Safety: Use thread-safe collections and synchronization
- Performance Overhead: Use thread pools and minimize context switches
- Debugging Complexity: Use proper logging and monitoring tools
Best Practices#
- Design for Thread Safety: Consider concurrency from the start
- Use Thread Pools: Avoid creating threads repeatedly
- Minimize Shared State: Reduce need for synchronization
- Handle Interruptions: Properly respond to thread interruption
- Monitor Performance: Profile and measure multithreaded applications
Real-world Applications#
Where Multithreading Excels#
- Web Servers: Handling multiple client requests
- Database Systems: Concurrent query processing
- Game Engines: Physics, rendering, AI, input handling
- Media Processing: Video/audio encoding and streaming
- Scientific Computing: Parallel algorithms and simulations
- User Interfaces: Background tasks and responsive UIs
layout: center class: text-center#
Thank You!#
Multithreading Concepts Complete#
Lecture 30 Successfully Completed!
You now understand the fundamentals of multithreading and concurrent programming

