Skip to content

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!


  1. Understanding Concurrency
  2. Processes vs Threads
  3. Java Threading Essentials
  4. Synchronization Deep Dive
  5. Threading Problems & Solutions
  6. Quick Reference

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)

Timeline: 0----1----2----3----4----5----6----7----8
Task A: [===] [===] [===]
Task B: [===] [===] [===]
Task C: [=========]
CPU rapidly switches between tasks - they APPEAR simultaneous
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 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

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)
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
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

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
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!

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
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

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
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
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
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!

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.


Problem 1: Deadlock - “Mexican Standoff”

Section titled “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”

Section titled “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!”

Section titled “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.


NEW ──start()──→ RUNNABLE ──scheduler──→ RUNNING
↑ ↓
WAITING ←──notify()──── SLEEPING
↑ ↓
BLOCKED ←──────────────→ TERMINATED
TechniqueUse WhenProsCons
synchronized methodSimple casesEasy to useCan be slow
synchronized blockMost casesPrecise controlNeed to choose lock object
Multiple locksComplex objectsBetter performanceDeadlock risk
  • 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
  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