Skip to content
advancedPhase 17 · Java Multithreading

Synchronization

Use synchronized keyword and locks to prevent race conditions.

1h
2 problems
Topic Progress0%

Race Conditions

Race Conditions

A race condition occurs when two or more threads access shared data concurrently and the result depends on the timing of their execution.

public class RaceCondition {
    private int count = 0;
    
    public void increment() {
        count++;  // This is NOT atomic!
        // count++ is actually: temp = count; count = temp + 1;
    }
    
    public static void main(String[] args) throws InterruptedException {
        RaceCondition rc = new RaceCondition();
        
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) rc.increment();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) rc.increment();
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println(rc.count);  // Expected: 200000, Actual: varies!
    }
}

Why count++ is not atomic:

  1. Read count from memory → register
  2. Increment register value
  3. Write register back to memory

If two threads interleave:

Thread 1: reads count=5
Thread 2: reads count=5
Thread 1: writes count=6
Thread 2: writes count=6  // Lost update!

Synchronized Keyword

synchronized Keyword

synchronized ensures that only one thread can execute a critical section at a time.

Synchronized Method:

public class Counter {
    private int count = 0;
    
    public synchronized void increment() {
        count++;  // Now atomic - only one thread at a time
    }
    
    public synchronized int getCount() {
        return count;
    }
}

Synchronized Block:

public class BankAccount {
    private int balance;
    private final Object lock = new Object();
    
    public void transfer(BankAccount target, int amount) {
        synchronized (lock) {  // Lock on specific object
            if (balance >= amount) {
                balance -= amount;
                target.deposit(amount);
            }
        }
    }
    
    public void deposit(int amount) {
        synchronized (this) {  // Lock on this object
            balance += amount;
        }
    }
}

Synchronized on different objects:

public class SharedResource {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    
    public void method1() {
        synchronized (lock1) {
            // Only one thread can be here at a time
        }
    }
    
    public void method2() {
        synchronized (lock2) {
            // Independent lock - can run in parallel with method1
        }
    }
}

Key rules:

  • Only one thread can hold a synchronized lock at a time
  • Lock is released when exiting synchronized block
  • Same lock object = mutual exclusion
  • Different lock objects = no mutual exclusion

Volatile Keyword

volatile Keyword

volatile ensures visibility of changes across threads. It does NOT provide atomicity.

public class VolatileDemo {
    private volatile boolean running = true;
    
    public void stop() {
        running = false;  // Visible to other threads immediately
    }
    
    public void run() {
        while (running) {  // Always reads from main memory
            // Do work
        }
        System.out.println("Stopped");
    }
    
    public static void main(String[] args) throws InterruptedException {
        VolatileDemo demo = new VolatileDemo();
        
        Thread worker = new Thread(demo::run);
        worker.start();
        
        Thread.sleep(1000);
        demo.stop();  // Worker thread will see this change
    }
}

volatile vs synchronized:

Feature volatile synchronized
Atomicity No Yes
Visibility Yes Yes
Mutual exclusion No Yes
Performance Fast Slower

When to use volatile:

  • Status flags (running, stopped)
  • Double-checked locking
  • Immutable objects

When NOT to use volatile:

  • Compound operations (count++)
  • When you need atomicity (use synchronized or Atomic classes)

Deadlocks

Deadlocks: Causes and Prevention

A deadlock occurs when two or more threads are blocked forever, each waiting for the other to release a lock.

public class DeadlockExample {
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();
    
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lockA) {
                System.out.println("Thread 1: Holding lockA");
                try { Thread.sleep(100); } catch (Exception e) {}
                
                System.out.println("Thread 1: Waiting for lockB");
                synchronized (lockB) {  // Waiting for lockB (held by t2)
                    System.out.println("Thread 1: Holding lockA and lockB");
                }
            }
        });
        
        Thread t2 = new Thread(() -> {
            synchronized (lockB) {
                System.out.println("Thread 2: Holding lockB");
                try { Thread.sleep(100); } catch (Exception e) {}
                
                System.out.println("Thread 2: Waiting for lockA");
                synchronized (lockA) {  // Waiting for lockA (held by t1)
                    System.out.println("Thread 2: Holding lockB and lockA");
                }
            }
        });
        
        t1.start();
        t2.start();
        // DEADLOCK! Both threads waiting forever
    }
}

Four conditions for deadlock:

  1. Mutual exclusion - Only one thread can hold a lock
  2. Hold and wait - Thread holds lock while waiting for another
  3. No preemption - Locks can't be forcibly taken
  4. Circular wait - Thread A waits for B, B waits for A

Prevention strategies:

  1. Lock ordering - Always acquire locks in same order
  2. Try lock - Use tryLock() with timeout
  3. Avoid nested locks - Minimize lock scope
  4. Lock timeout - Use tryLock() with timeout

Lock Interface and ReentrantLock

Lock Interface and ReentrantLock

ReentrantLock provides more flexibility than synchronized.

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;
    
    public void increment() {
        lock.lock();  // Acquire lock
        try {
            count++;  // Critical section
        } finally {
            lock.unlock();  // ALWAYS release in finally!
        }
    }
    
    // Try lock with timeout
    public boolean tryIncrement() throws InterruptedException {
        if (lock.tryLock(1, TimeUnit.SECONDS)) {
            try {
                count++;
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false;  // Failed to acquire lock
    }
    
    // Fair lock (threads acquire in FIFO order)
    private final ReentrantLock fairLock = new ReentrantLock(true);
    
    // ReadWriteLock
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    
    public int read() {
        rwLock.readLock().lock();  // Multiple readers allowed
        try {
            return count;
        } finally {
            rwLock.readLock().unlock();
        }
    }
    
    public void write(int value) {
        rwLock.writeLock().lock();  // Exclusive access
        try {
            count = value;
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

ReentrantLock features:

  • lock() / unlock() - Manual lock management
  • tryLock() - Non-blocking attempt
  • tryLock(timeout) - Timed attempt
  • lockInterruptibly() - Interruptible lock
  • Fair lock option (FIFO ordering)
  • ReadWriteLock for read-heavy workloads

When to use ReentrantLock:

  • Need tryLock() with timeout
  • Need fair ordering
  • Need multiple condition variables
  • ReadWriteLock for read-heavy scenarios

Practice Problems

0/2solved
Fix the Race Condition

Fix the race condition in this code. There are multiple correct solutions: ```java public class UnsafeCounter { private int count = 0; public void increment() { count++; } public int getCount() { return count; } } ```

Solution
**Solution 1: synchronized**
```java
public class SafeCounter {
    private int count = 0;
    
    public synchronized void increment() { count++; }
    public synchronized int getCount() { return count; }
}
```

**Solution 2: AtomicInteger**
```java
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);
    
    public void increment() { count.incrementAndGet(); }
    public int getCount() { return count.get(); }
}
```

AtomicInteger uses CAS (Compare-And-Swap) for lock-free thread safety.
Identify Deadlock Potential

Does this code have deadlock potential? If so, how would you fix it? ```java public class PaymentService { private final Object userLock = new Object(); private final Object paymentLock = new Object(); public void processPayment(User user, Payment payment) { synchronized (userLock) { synchronized (paymentLock) { // Process payment } } } public void refund(User user, Payment payment) { synchronized (paymentLock) { synchronized (userLock) { // Process refund } } } } ```

Solution
**Yes, deadlock potential!**

- `processPayment` acquires: userLock → paymentLock
- `refund` acquires: paymentLock → userLock

If both methods run simultaneously:
- Thread 1 holds userLock, waits for paymentLock
- Thread 2 holds paymentLock, waits for userLock
- DEADLOCK!

**Fix: Lock ordering**
```java
public void processPayment(User user, Payment payment) {
    synchronized (userLock) {
        synchronized (paymentLock) {
            // Process payment
        }
    }
}

public void refund(User user, Payment payment) {
    synchronized (userLock) {  // Same order!
        synchronized (paymentLock) {
            // Process refund
        }
    }
}
```

Always acquire locks in the same order across all methods.

Quiz

1. What is a race condition?

Question 1 options

2. What does the volatile keyword guarantee?

Question 2 options

3. Which is a prevention strategy for deadlocks?

Question 3 options

4. What is the difference between synchronized and ReentrantLock?

Question 4 options

Flashcards

Question

What is a race condition?

Answer

A bug where the program's behavior depends on the relative timing of multiple threads accessing shared data.

Question

What does volatile do?

Answer

Ensures visibility of writes across threads. Does NOT provide atomicity or mutual exclusion.

Question

What are the four conditions for deadlock?

Answer

Mutual exclusion, Hold and wait, No preemption, Circular wait.

Question

How do you prevent deadlocks?

Answer

Lock ordering (same order everywhere), tryLock with timeout, avoid nested locks, lock timeout.

Question

What is Synchronization and Concurrency?

Answer

Synchronization and Concurrency is a key concept in Java programming.

Revision Notes

Key Takeaways

  • 1.count++ is not atomic - needs synchronization
  • 2.volatile ensures visibility, not atomicity
  • 3.Deadlocks require four conditions - break any one to prevent
  • 4.Always acquire locks in the same order

Interview Tips

  • Explain why count++ is not atomic (read-modify-write)
  • Differentiate volatile (visibility) from synchronized (atomicity + visibility)
  • List the four deadlock conditions
  • Explain lock ordering as a deadlock prevention strategy

Cheat Sheet

Synchronization Cheat Sheet

Race Condition

  • Two threads modify shared data
  • Result depends on timing
  • Fix: synchronized, AtomicInteger

synchronized

  • Method: public synchronized void method()
  • Block: synchronized (lock) { ... }
  • Only one thread per lock

volatile

  • Visibility only (not atomicity)
  • Use for status flags
  • Don't use for count++

Deadlock

  • Two threads waiting for each other's locks
  • Fix: lock ordering, tryLock, timeout

ReentrantLock

  • More flexible than synchronized
  • tryLock(), lockInterruptibly()
  • Fair lock option