Skip to content

Synchronization and Locks

Synchronization is essential in concurrent programming to ensure correct access to shared resources and to avoid data races, visibility problems, and other hazards. Java provides both intrinsic (built-in) and explicit locking mechanisms.


When multiple threads access shared mutable data, synchronization ensures:

  • Mutual exclusion: Only one thread can access a critical section at a time
  • Visibility: Changes made by one thread are visible to others
  • Ordering: Operations happen in a predictable order

Intrinsic Locks and the synchronized Keyword

Section titled “Intrinsic Locks and the synchronized Keyword”

Every Java object has an intrinsic lock (monitor). The synchronized keyword acquires this lock for a block or method:

public synchronized void increment() {
count++;
}
public void increment() {
synchronized(this) {
count++;
}
}
public static synchronized void staticMethod() {
// Locks on the Class object
}

Drawbacks:

  • Only exclusive locking (no read/write distinction)
  • Can lead to contention and poor scalability

Explicit Locks: Lock, ReentrantLock, ReadWriteLock

Section titled “Explicit Locks: Lock, ReentrantLock, ReadWriteLock”

Java’s java.util.concurrent.locks package provides more flexible locking mechanisms.

import java.util.concurrent.locks.*;
Lock lock = new ReentrantLock();
lock.lock();
try {
// critical section
} finally {
lock.unlock();
}

Features of ReentrantLock:

  • Try-lock mechanism: Non-blocking attempt to acquire lock via tryLock() method
  • Timed locking: Attempt to acquire lock with timeout using tryLock(long time, TimeUnit unit)
  • Interruptible acquisition: lockInterruptibly() allows threads to respond to interrupts while waiting
  • Fairness policies: Constructor option ReentrantLock(boolean fair) to enforce first-come, first-served ordering
  • Reentrancy: Same thread can acquire the lock multiple times (must release same number of times)
  • Lock querying: Methods like isHeldByCurrentThread(), getHoldCount(), and isLocked()
  • Multiple conditions: Support for multiple wait-sets using newCondition()
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
readLock.lock();
try {
// read-only section
} finally {
readLock.unlock();
}
writeLock.lock();
try {
// write section
} finally {
writeLock.unlock();
}
  1. Cache Implementation
class Cache<K, V> {
private final Map<K, V> cache = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public V get(K key) {
rwLock.readLock().lock();
try {
return cache.get(key);
} finally {
rwLock.readLock().unlock();
}
}
public void put(K key, V value) {
rwLock.writeLock().lock();
try {
cache.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
}
  1. Frequently Read, Rarely Updated Configuration
class Configuration {
private Map<String, String> settings = new HashMap<>();
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
public String getSetting(String key) {
rwLock.readLock().lock();
try {
return settings.get(key);
} finally {
rwLock.readLock().unlock();
}
}
public void updateSettings(Map<String, String> newSettings) {
rwLock.writeLock().lock();
try {
settings.putAll(newSettings);
} finally {
rwLock.writeLock().unlock();
}
}
}

Declaring a variable as volatile ensures that:

  • Writes to the variable are immediately visible to other threads
  • No caching of the variable occurs in thread-local memory
private volatile boolean running = true;
public void stop() {
running = false;
}
public void run() {
while (running) {
// do work
}
}

Limitations:

  • Does not guarantee atomicity (e.g., count++ is not atomic)
  • Only useful for simple flags or single variable state

Defines the ordering of operations in concurrent programs. If action A happens-before action B, then the effects of A are visible to B.

Key happens-before guarantees:

  • Lock release happens-before subsequent lock acquisition
  • Writes to a volatile variable happen-before subsequent reads
  • Thread start happens-before actions in the started thread
  • Thread join happens-before the join returns

Concurrency Hazards: Deadlock, Livelock, Starvation

Section titled “Concurrency Hazards: Deadlock, Livelock, Starvation”

Occurs when two or more threads are waiting for each other to release locks, resulting in a standstill.

Example:

// Thread 1
synchronized(lockA) {
synchronized(lockB) { }
}
// Thread 2
synchronized(lockB) {
synchronized(lockA) { }
}

Threads keep changing state in response to each other but cannot make progress.

A thread is perpetually denied access to resources and cannot proceed.


  • Always release locks in a finally block
  • Minimize the scope of synchronized blocks
  • Prefer higher-level abstractions (java.util.concurrent classes)
  • Avoid holding locks while calling external methods
  • Use volatile only for simple cases
  • Avoid nested locks or always acquire locks in a consistent order
  • Use timeouts for lock acquisition where possible

class Counter {
private int count = 0;
public synchronized void increment() { count++; }
public synchronized int get() { return count; }
}
Lock lock = new ReentrantLock();
int count = 0;
void increment() {
lock.lock();
try { count++; } finally { lock.unlock(); }
}
ReadWriteLock rwLock = new ReentrantReadWriteLock();
int value;
void write(int v) {
rwLock.writeLock().lock();
try { value = v; } finally { rwLock.writeLock().unlock(); }
}
int read() {
rwLock.readLock().lock();
try { return value; } finally { rwLock.readLock().unlock(); }
}
volatile boolean running = true;
void stop() { running = false; }
void run() { while (running) { /* work */ } }