Guide to Concurrency and Multithreading

Program Construction 2nd Semester

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

  1. Understanding Concurrency
  2. Processes vs Threads
  3. Java Threading Essentials
  4. Synchronization Deep Dive
  5. Threading Problems & Solutions
  6. 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
BenefitExampleReal-World Impact
Better Resource UtilizationWhile Task A waits for disk read, Task B uses CPUUp to 80% better performance
Improved ResponsivenessUI stays active while saving large filesBetter user experience
Higher ThroughputWeb server handles 1000s of requestsMore users served
Challenges of Concurrency
ChallengeWhat Goes WrongSimple Analogy
Race ConditionsTwo threads modify same dataTwo people editing same document
Data InconsistencyThreads see different valuesBank balance showing different amounts
DeadlocksThreads wait for each other foreverTwo 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...
ScenarioWhy Process?Example
Need SecurityIsolation prevents crashesEach browser tab as separate process
Different AppsCompletely separate functionalityWord processor + Media player
Can Afford OverheadMore memory usage is acceptableDesktop applications
Distributed SystemsRunning on different machinesMicroservices
Use Threads When...
ScenarioWhy Thread?Example
Frequent Data SharingSame memory spaceGUI app (UI + background work)
Performance CriticalLower overheadGame engine (rendering + physics)
Coordinated TasksNeed tight cooperationWeb server handling requests
Resource EfficientLess memory usageMobile applications

Java Threading Essentials

Thread Lifecycle Journey

NEW → RUNNABLE → RUNNING → TERMINATED
  ↓        ↑         ↓
  └→ SLEEPING ←→ BLOCKED ←→ WAITING
Thread States Explained
StateWhat’s HappeningAnalogy
NEWThread created but not startedPerson hired but not started work
RUNNABLEReady to run, waiting for CPUPerson ready to work, waiting for assignment
RUNNINGCurrently executingPerson actively working
SLEEPINGVoluntarily paused for timePerson taking a scheduled break
BLOCKEDWaiting for lockPerson waiting for meeting room key
WAITINGWaiting for another threadPerson waiting for colleague to finish
TERMINATEDFinished executionPerson 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
MethodPurposeExample Use Case
start()Begin thread executionLaunch background task
join()Wait for thread to finishWait for file download before processing
sleep()Pause for specified timeAnimation delays
interrupt()Signal thread to stopCancel long-running operation
setPriority()Set thread importanceUI 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

TechniqueUse WhenProsCons
synchronized methodSimple casesEasy to useCan be slow
synchronized blockMost casesPrecise controlNeed to choose lock object
Multiple locksComplex objectsBetter performanceDeadlock 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

  1. Minimize synchronized sections - Lock only what you must
  2. Prefer Runnable over Thread - Better flexibility
  3. Use private lock objects - Avoid external interference
  4. Don’t hold locks longer than necessary - Release quickly
  5. 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