Guide to Concurrency and Multithreading
Quick Check: Have you ever wondered why your computer can play music, browse the web, and download files all at the same time? That’s concurrency in action!
Table of Contents
- Understanding Concurrency
- Processes vs Threads
- Java Threading Essentials
- Synchronization Deep Dive
- Threading Problems & Solutions
- Quick Reference
Understanding Concurrency
What’s the Difference?
Click to reveal: Concurrency vs Parallelism
Concurrency = Juggling multiple balls (dealing with multiple things at once) Parallelism = Having multiple people each juggle one ball (actually doing multiple things simultaneously)
Visual Comparison
Single-Core CPU (Concurrency Only)
Timeline: 0----1----2----3----4----5----6----7----8
Task A: [===] [===] [===]
Task B: [===] [===] [===]
Task C: [=========]
CPU rapidly switches between tasks - they APPEAR simultaneous
Multi-Core CPU (True Parallelism)
Timeline: 0----1----2----3----4----5----6----7----8
Core 1: Task A [========================]
Core 2: Task B [========================]
Core 3: Task C [========================]
Tasks ACTUALLY run at the same time
Benefits & Challenges
Benefits of Concurrency
Benefit | Example | Real-World Impact |
---|---|---|
Better Resource Utilization | While Task A waits for disk read, Task B uses CPU | Up to 80% better performance |
Improved Responsiveness | UI stays active while saving large files | Better user experience |
Higher Throughput | Web server handles 1000s of requests | More users served |
Challenges of Concurrency
Challenge | What Goes Wrong | Simple Analogy |
---|---|---|
Race Conditions | Two threads modify same data | Two people editing same document |
Data Inconsistency | Threads see different values | Bank balance showing different amounts |
Deadlocks | Threads wait for each other forever | Two cars blocking each other |
Processes vs Threads
The House Analogy
Think of your computer as a neighborhood:
Process = Entire House
Process (Web Browser)
├── Own entrance (memory space)
├── Own security system (isolation)
├── Own utilities (system resources)
└── Residents (threads)
Each house (process) has:
- Own memory space (isolated from neighbors)
- Own resources (files, network connections)
- House number (Process ID)
- At least one resident (main thread)
Thread = Person in House
Thread (Browser Tab)
├── Own bedroom (stack)
├── Own thoughts (program counter)
├── Own notes (registers)
└── Shares common areas (heap memory)
People in same house (threads) share:
- Kitchen (heap memory)
- Living room (file handles)
- House resources
But each person has:
- Own bedroom (stack)
- Own thoughts (program counter)
- Own ID (Thread ID)
System Architecture Example
Computer System
├── Process 1 (Web Browser)
│ ├── Main UI Thread
│ ├── Tab 1 Thread
│ ├── Tab 2 Thread
│ └── Downloads Thread
├── Process 2 (Music Player)
│ ├── Audio Thread
│ ├── UI Thread
│ └── Network Thread
└── Process 3 (Text Editor)
├── Main Thread
└── Auto-save Thread
Decision Matrix: When to Use What?
Use Processes When...
Scenario | Why Process? | Example |
---|---|---|
Need Security | Isolation prevents crashes | Each browser tab as separate process |
Different Apps | Completely separate functionality | Word processor + Media player |
Can Afford Overhead | More memory usage is acceptable | Desktop applications |
Distributed Systems | Running on different machines | Microservices |
Use Threads When...
Scenario | Why Thread? | Example |
---|---|---|
Frequent Data Sharing | Same memory space | GUI app (UI + background work) |
Performance Critical | Lower overhead | Game engine (rendering + physics) |
Coordinated Tasks | Need tight cooperation | Web server handling requests |
Resource Efficient | Less memory usage | Mobile applications |
Java Threading Essentials
Thread Lifecycle Journey
NEW → RUNNABLE → RUNNING → TERMINATED
↓ ↑ ↓
└→ SLEEPING ←→ BLOCKED ←→ WAITING
Thread States Explained
State | What’s Happening | Analogy |
---|---|---|
NEW | Thread created but not started | Person hired but not started work |
RUNNABLE | Ready to run, waiting for CPU | Person ready to work, waiting for assignment |
RUNNING | Currently executing | Person actively working |
SLEEPING | Voluntarily paused for time | Person taking a scheduled break |
BLOCKED | Waiting for lock | Person waiting for meeting room key |
WAITING | Waiting for another thread | Person waiting for colleague to finish |
TERMINATED | Finished execution | Person completed work and left |
Two Ways to Create Threads
Method 1: Extending Thread Class
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running: " + getName());
}
}
// Usage
MyThread thread = new MyThread();
thread.start(); // Launch the thread
Method 2: Implementing Runnable (Recommended)
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("Task running: " + Thread.currentThread().getName());
}
}
// Usage
Thread thread = new Thread(new MyTask());
thread.start(); // Launch the thread
Why Runnable is better: Java single inheritance limitation - you can implement multiple interfaces!
Thread Control Methods
Essential Thread Methods
Method | Purpose | Example Use Case |
---|---|---|
start() | Begin thread execution | Launch background task |
join() | Wait for thread to finish | Wait for file download before processing |
sleep() | Pause for specified time | Animation delays |
interrupt() | Signal thread to stop | Cancel long-running operation |
setPriority() | Set thread importance | UI threads get higher priority |
Special Thread Types
Daemon Threads
What are they? Background helper threads that die when main program ends.
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println("Cleaning up...");
try { Thread.sleep(1000); } catch (InterruptedException e) { break; }
}
});
daemonThread.setDaemon(true); // Make it a daemon
daemonThread.start();
// When main program ends, daemon thread automatically stops
Real Examples:
- Garbage collector
- Timer threads
- Monitoring threads
Synchronization Deep Dive
The Problem: Race Conditions
What Goes Wrong Without Synchronization?
class BankAccount {
private int balance = 1000;
// DANGER: Not synchronized!
public void withdraw(int amount) {
if (balance >= amount) { // Thread A checks: 1000 >= 500 ✓
// Thread B checks: 1000 >= 700 ✓
balance = balance - amount; // Thread A: 1000 - 500 = 500
// Thread B: 1000 - 700 = 300
}
}
}
// Result: Balance could be 500 OR 300, depending on timing!
// Expected: Should reject one withdrawal when balance < 700
Solution 1: Synchronized Methods
Method-Level Synchronization
class BankAccount {
private int balance = 1000;
// Only ONE thread can access this method at a time
public synchronized void withdraw(int amount) {
if (balance >= amount) {
System.out.println("Withdrawing: " + amount);
balance = balance - amount;
System.out.println("New balance: " + balance);
} else {
System.out.println("Insufficient funds!");
}
}
public synchronized int getBalance() {
return balance; // Also synchronized for consistency
}
}
Key Points:
- Locks the entire object for the method duration
- Other synchronized methods must wait
- Thread-safe but potentially slower
Solution 2: Synchronized Blocks (Better!)
Block-Level Synchronization
class BankAccount {
private int balance = 1000;
private String accountInfo = "John Doe";
public void withdraw(int amount) {
// Multiple threads can enter method
System.out.println("Processing withdrawal request...");
synchronized(this) { // Only critical section is locked
if (balance >= amount) {
balance = balance - amount;
}
} // Lock released immediately
System.out.println("Transaction logged");
}
}
Advantages:
- Faster: Only locks critical sections
- More precise control
- Better concurrency
Advanced: Fine-Grained Synchronization
Multiple Locks for Different Resources
public class SmartBankAccount {
private int balance = 1000;
private String accountInfo = "John Doe";
private List<String> transactionHistory = new ArrayList<>();
// Separate locks for different resources
private final Object balanceLock = new Object();
private final Object infoLock = new Object();
private final Object historyLock = new Object();
public void withdraw(int amount) {
synchronized(balanceLock) { // Only locks balance operations
if (balance >= amount) {
balance -= amount;
}
}
}
public void updateInfo(String newInfo) {
synchronized(infoLock) { // Different lock for account info
accountInfo = newInfo;
}
}
public void addTransaction(String transaction) {
synchronized(historyLock) { // Another lock for history
transactionHistory.add(transaction);
}
}
// These three methods can run SIMULTANEOUSLY!
}
Performance Boost: Operations on different data can run in parallel!
Reentrant Synchronization
Same Thread, Multiple Locks
class ReentrantExample {
public synchronized void methodA() {
System.out.println("In methodA");
methodB(); // Can same thread call another synchronized method?
}
public synchronized void methodB() {
System.out.println("In methodB");
// YES! Same thread can acquire same lock multiple times
}
}
Why it works: Java tracks lock ownership by thread, not just lock acquisition.
Threading Problems & Solutions
Problem 1: Deadlock - “Mexican Standoff”
The Classic Deadlock Scenario
class DeadlockDemo {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void method1() {
synchronized(lock1) { // Thread A gets lock1
System.out.println("Thread A: Got lock1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized(lock2) { // Thread A waits for lock2
System.out.println("Thread A: Got lock2");
}
}
}
public void method2() {
synchronized(lock2) { // Thread B gets lock2
System.out.println("Thread B: Got lock2");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized(lock1) { // Thread B waits for lock1
System.out.println("Thread B: Got lock1");
}
}
}
}
// Result: Both threads wait forever!
Deadlock Prevention: Lock Ordering
class DeadlockFixed {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void method1() {
synchronized(lock1) { // Always acquire lock1 first
synchronized(lock2) { // Then lock2
System.out.println("Thread A: Both locks acquired");
}
}
}
public void method2() {
synchronized(lock1) { // Same order: lock1 first
synchronized(lock2) { // Then lock2
System.out.println("Thread B: Both locks acquired");
}
}
}
}
// Result: Sequential execution, no deadlock!
Golden Rule: Always acquire locks in the same order!
Problem 2: Starvation - “Never My Turn”
When Low Priority Threads Never Get Resources
class StarvationExample {
public synchronized void importantWork() {
// High priority threads keep calling this
System.out.println("High priority work");
}
public synchronized void backgroundWork() {
// Low priority threads never get a chance
System.out.println("Background work");
}
}
Solutions:
- Use fair locks:
ReentrantLock(true)
- Time-bounded waiting
- Priority scheduling algorithms
Problem 3: Livelock - “After You!” “No, After You!”
The Overly Polite Threads
class LivelockExample {
static class PoliteWorker {
private String name;
private boolean active;
public PoliteWorker(String name) {
this.name = name;
this.active = true;
}
public void work(PoliteWorker colleague) {
while (active) {
if (colleague.active) {
System.out.println(name + ": After you, " + colleague.name);
active = false; // I'll step back
try { Thread.sleep(100); } catch (InterruptedException e) {}
active = true; // Let me try again
}
}
}
}
// Both workers keep stepping aside for each other!
// They're not blocked, but no work gets done
}
Solution: Add randomization or timeout to break the cycle.
Quick Reference
Thread States Cheat Sheet
NEW ──start()──→ RUNNABLE ──scheduler──→ RUNNING
↑ ↓
WAITING ←──notify()──── SLEEPING
↑ ↓
BLOCKED ←──────────────→ TERMINATED
Synchronization Quick Guide
Technique | Use When | Pros | Cons |
---|---|---|---|
synchronized method | Simple cases | Easy to use | Can be slow |
synchronized block | Most cases | Precise control | Need to choose lock object |
Multiple locks | Complex objects | Better performance | Deadlock risk |
Problem Prevention Checklist
- Deadlock: Always acquire locks in same order
- Starvation: Use fair locks or time limits
- Livelock: Add randomization to retry logic
- Race Conditions: Synchronize shared resource access
Best Practices Summary
- Minimize synchronized sections - Lock only what you must
- Prefer Runnable over Thread - Better flexibility
- Use private lock objects - Avoid external interference
- Don’t hold locks longer than necessary - Release quickly
- Test thoroughly - Concurrency bugs are sneaky!
Congratulations! You’ve mastered the fundamentals of Java concurrency and multithreading. Remember: with great power comes great responsibility - use synchronization wisely!
Next Steps:
- Explore
java.util.concurrent
package - Learn about Thread Pools and ExecutorService
- Study Lock-free programming techniques
- Practice with real-world concurrency problems