Skip to content

Synchronization & Thread Safety in Java

Concurrency introduces challenges when multiple threads access and modify shared state at the same time. Without proper safeguards, you risk race conditions, inconsistent data, deadlocks, or subtle memory visibility bugs. This article explores the tools Java provides to ensure thread safety.

1. Shared Mutable State

  • Problem: When multiple threads modify the same variable/object concurrently, results can be unpredictable.
  • Example:
java
class Counter {
    private int count = 0;
    public void increment() { count++; }
    public int getCount() { return count; }
}

Counter c = new Counter();
for (int i = 0; i < 1000; i++) {
    new Thread(c::increment).start();
}
System.out.println(c.getCount()); // Not always 1000!
  • Cause: Increment (count++) is not atomic: it involves read → modify → write, which can interleave across threads.

2. synchronized Keyword

Java’s simplest synchronization mechanism.

  • Method synchronization:
java
class Counter {
    private int count = 0;
    public synchronized void increment() { count++; }
    public synchronized int getCount() { return count; }
}
  • Block synchronization:
java
synchronized(this) {
    // critical section
}
  • Pros:
    • Easy to use.
    • Automatically provides mutual exclusion and memory visibility guarantees.
  • Cons:
    • Can cause blocking and reduce throughput.
    • Prone to deadlocks if multiple locks are nested.

3. Locks (ReentrantLock, ReadWriteLock)

The java.util.concurrent.locks package provides more advanced locking.

ReentrantLock

java
import java.util.concurrent.locks.*;

class Counter {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
    public int getCount() { return count; }
}
  • Pros:
    • More flexible (supports tryLock(), lock fairness).
    • Can break out of deadlock with timeout.
  • Cons:
    • More verbose; must always unlock in finally.
    • Easier to misuse compared to synchronized.

ReadWriteLock

  • Allows multiple readers but only one writer.
java
ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock();
// read data
rwLock.readLock().unlock();
  • Useful when reads dominate writes.

4. volatile and Memory Visibility

  • Ensures that updates to a variable are always visible to other threads.
  • Does not guarantee atomicity.
  • Example:
java
volatile boolean flag = true;

public void stop() {
    flag = false; // visible across threads
}
  • Use Case: Flags, simple state indicators.
  • Not safe for compound operations (e.g., count++).

5. Atomic Classes

The java.util.concurrent.atomic package provides lock-free, thread-safe classes.

  • Example:
java
import java.util.concurrent.atomic.AtomicInteger;

class Counter {
    private AtomicInteger count = new AtomicInteger(0);
    public void increment() { count.incrementAndGet(); }
    public int getCount() { return count.get(); }
}
  • Pros:
    • Fast and scalable for simple atomic operations.
  • Cons:
    • Limited to basic operations (increment, CAS).
    • For complex updates, locks may still be needed.

6. Trade-Offs

ToolGuaranteesProsConsUse Cases
synchronizedMutual exclusion + visibilitySimple, built-inBlocking, risk of deadlockCritical sections, simple thread safety
ReentrantLockMutual exclusion + visibilityFlexible, tryLock(), fairness optionsVerbose, manual unlockAdvanced locking scenarios
ReadWriteLockMultiple readers, 1 writerGreat for read-heavy workloadsWriters can starveCaches, configs, databases
volatileVisibility onlyLightweight, non-blockingNo atomicityFlags, status variables
Atomic classesAtomic operations, CAS-basedLock-free, fastLimited operationsCounters, accumulators

Conclusion

Java offers multiple synchronization tools depending on your needs:

  • Use synchronized for simplicity.
  • Use ReentrantLock when you need fine-grained control.
  • Use volatile for visibility of simple flags.
  • Use Atomic classes for lock-free counters and basic operations.
  • Always balance performance vs. correctness.