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:
- Read count from memory → register
- Increment register value
- 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:
- Mutual exclusion - Only one thread can hold a lock
- Hold and wait - Thread holds lock while waiting for another
- No preemption - Locks can't be forcibly taken
- Circular wait - Thread A waits for B, B waits for A
Prevention strategies:
- Lock ordering - Always acquire locks in same order
- Try lock - Use tryLock() with timeout
- Avoid nested locks - Minimize lock scope
- 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 managementtryLock()- Non-blocking attempttryLock(timeout)- Timed attemptlockInterruptibly()- 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
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.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?
2. What does the volatile keyword guarantee?
3. Which is a prevention strategy for deadlocks?
4. What is the difference between synchronized and ReentrantLock?
Flashcards
Question
What is a race condition?
Click to reveal answer
Answer
A bug where the program's behavior depends on the relative timing of multiple threads accessing shared data.
Question
What does volatile do?
Click to reveal answer
Answer
Ensures visibility of writes across threads. Does NOT provide atomicity or mutual exclusion.
Question
What are the four conditions for deadlock?
Click to reveal answer
Answer
Mutual exclusion, Hold and wait, No preemption, Circular wait.
Question
How do you prevent deadlocks?
Click to reveal answer
Answer
Lock ordering (same order everywhere), tryLock with timeout, avoid nested locks, lock timeout.
Question
What is Synchronization and Concurrency?
Click to reveal answer
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