foreach-ui logo
codeLanguages
account_treeDSA

Quick Actions

quizlock Random Quiz
trending_uplock Progress
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • quiz
Java
  • Understand race conditions and thread safety issues
  • Master the synchronized keyword for methods and blocks
  • Learn the volatile keyword and memory visibility
  • Understand deadlock and how to prevent it

Synchronization Basics

Multiple threads accessing shared data? Chaos. Two people withdrawing from the same bank account simultaneously—without coordination, the balance corrupts. Synchronization ensures threads access shared resources safely.

The Problem: Race Conditions

A race condition occurs when the outcome depends on unpredictable thread timing:

public class Counter {
    private int count = 0;
    
    public void increment() {
        count++;  // Looks atomic, but it's NOT!
    }
    
    public int getCount() {
        return count;
    }
}

The count++ operation is actually three steps:

  1. Read the current value of count
  2. Add 1 to that value
  3. Write the new value back

If two threads execute simultaneously:

Thread 1: Read count (0)
Thread 2: Read count (0)     <- Both read 0!
Thread 1: Add 1 (result: 1)
Thread 2: Add 1 (result: 1)  <- Both compute 1!
Thread 1: Write 1
Thread 2: Write 1            <- Final count: 1 (should be 2!)

The synchronized Keyword

The synchronized keyword ensures that only one thread can execute a block of code at a time:

Synchronized Methods

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

When a thread enters a synchronized method:

  1. It acquires the lock on the object (this)
  2. Other threads must wait to enter any synchronized method on the same object
  3. The lock is released when the method exits

Synchronized Blocks

For finer control, synchronize only the critical section:

public class Counter {
    private int count = 0;
    private final Object lock = new Object();
    
    public void increment() {
        // Non-critical code can run in parallel
        doSomeWork();
        
        synchronized (lock) {
            // Only this block is synchronized
            count++;
        }
        
        // More non-critical code
        doMoreWork();
    }
}

Choosing the Lock Object

// Using 'this' as lock (same as synchronized method)
synchronized (this) {
    // ...
}

// Using a dedicated lock object (recommended)
private final Object lock = new Object();
synchronized (lock) {
    // ...
}

// Using the class object (for static methods)
synchronized (Counter.class) {
    // ...
}

Best practice: Use a private dedicated lock object to prevent external code from interfering with your synchronization.

What Gets Locked?

Understanding what synchronized actually locks is crucial:

public class BankAccount {
    private double balance;
    
    // Locks on 'this' (the BankAccount instance)
    public synchronized void deposit(double amount) {
        balance += amount;
    }
    
    public synchronized void withdraw(double amount) {
        balance -= amount;
    }
}

Two threads calling methods on the same BankAccount instance are synchronized. But threads calling methods on different BankAccount instances run in parallel.

BankAccount account1 = new BankAccount();
BankAccount account2 = new BankAccount();

// These block each other (same object)
thread1.deposit(100);  // on account1
thread2.withdraw(50);  // on account1

// These run in parallel (different objects)
thread3.deposit(100);  // on account1
thread4.deposit(100);  // on account2

Static Synchronized Methods

Static methods synchronize on the Class object:

public class GlobalCounter {
    private static int count = 0;
    
    // Locks on GlobalCounter.class
    public static synchronized void increment() {
        count++;
    }
    
    // Equivalent to:
    public static void incrementExplicit() {
        synchronized (GlobalCounter.class) {
            count++;
        }
    }
}

Memory Visibility

Synchronization isn't just about mutual exclusion - it also guarantees memory visibility:

public class SharedFlag {
    private boolean running = true;
    
    public void stop() {
        running = false;  // Written by Thread 1
    }
    
    public void run() {
        while (running) {  // Read by Thread 2
            // Without synchronization, Thread 2 might never see the change!
            doWork();
        }
    }
}

Without synchronization, a thread might cache variables and never see updates from other threads. The synchronized keyword establishes a "happens-before" relationship that ensures visibility.

The volatile Keyword

For simple flags, volatile is a lighter-weight alternative:

public class SharedFlag {
    private volatile boolean running = true;  // Guaranteed visibility
    
    public void stop() {
        running = false;
    }
    
    public void run() {
        while (running) {  // Always sees the latest value
            doWork();
        }
    }
}

volatile guarantees:

  • Every read sees the latest write
  • No caching of the variable

But volatile doesn't help with compound operations:

private volatile int count = 0;
count++;  // Still not thread-safe! (read-modify-write)

Avoiding Common Pitfalls

1. Synchronizing on the Wrong Object

// WRONG - synchronizing on a local variable
public void badMethod() {
    Object lock = new Object();  // New object each call!
    synchronized (lock) {
        // Not actually protecting anything
    }
}

// CORRECT - synchronizing on a shared object
private final Object lock = new Object();
public void goodMethod() {
    synchronized (lock) {
        // Properly protected
    }
}

2. Synchronizing on Mutable References

// WRONG - the lock object can change
private String name = "initial";
public void setName(String n) {
    synchronized (name) {  // Different threads might lock on different objects!
        name = n;
    }
}

// CORRECT - use a dedicated final lock
private final Object nameLock = new Object();
public void setName(String n) {
    synchronized (nameLock) {
        name = n;
    }
}

3. Over-Synchronization

// Unnecessarily broad synchronization
public synchronized void process(List<Item> items) {
    for (Item item : items) {
        validateItem(item);     // Doesn't need synchronization
        transformItem(item);    // Doesn't need synchronization
        synchronized (this) {
            saveItem(item);     // Only this needs synchronization
        }
    }
}

Reentrant Locks

Java's intrinsic locks are reentrant - a thread can acquire the same lock multiple times:

public class ReentrantExample {
    public synchronized void outer() {
        System.out.println("In outer");
        inner();  // Same thread can enter another synchronized method
    }
    
    public synchronized void inner() {
        System.out.println("In inner");
    }
}

Without reentrancy, the second call would deadlock waiting for a lock the thread already holds.

Deadlock

Deadlock occurs when threads wait for each other's locks forever:

public class DeadlockExample {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    
    public void method1() {
        synchronized (lock1) {
            sleep(100);  // Give other thread time to grab lock2
            synchronized (lock2) {
                // Do work
            }
        }
    }
    
    public void method2() {
        synchronized (lock2) {  // Acquires lock2 first
            sleep(100);
            synchronized (lock1) {  // Waits for lock1... forever!
                // Do work
            }
        }
    }
}

Preventing Deadlock

  1. Always acquire locks in the same order:
// Both methods acquire lock1 first, then lock2
synchronized (lock1) {
    synchronized (lock2) { ... }
}
  1. Use tryLock with timeout (with Lock interface):
if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        // Do work
    } finally {
        lock.unlock();
    }
} else {
    // Couldn't acquire lock, handle gracefully
}

Race conditions happen when threads access shared mutable data unsafely. synchronized provides mutual exclusion and memory visibility. Always synchronize on the same shared object. Use volatile for simple flags that don't need compound operations. Avoid deadlock by acquiring locks in consistent order. Prefer synchronized blocks over methods for better granularity. Use private final lock objects for encapsulation. Reentrant locks let the same thread acquire the lock multiple times.

© 2026 forEach. All rights reserved.

Privacy Policy•Terms of Service