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.
Why Synchronization?
Section titled “Why Synchronization?”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:
Synchronized Methods
Section titled “Synchronized Methods”public synchronized void increment() { count++;}Synchronized Blocks
Section titled “Synchronized Blocks”public void increment() { synchronized(this) { count++; }}Static Synchronization
Section titled “Static Synchronization”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.
Lock and ReentrantLock
Section titled “Lock and ReentrantLock”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(), andisLocked() - Multiple conditions: Support for multiple wait-sets using
newCondition()
ReadWriteLock
Section titled “ReadWriteLock”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();}ReentrantReadWriteLock Use Cases:
Section titled “ReentrantReadWriteLock Use Cases:”- 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(); } }}- 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(); } }}The volatile Keyword
Section titled “The volatile Keyword”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
Happens-Before Relationship
Section titled “Happens-Before Relationship”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
volatilevariable 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”Deadlock
Section titled “Deadlock”Occurs when two or more threads are waiting for each other to release locks, resulting in a standstill.
Example:
// Thread 1synchronized(lockA) { synchronized(lockB) { }}// Thread 2synchronized(lockB) { synchronized(lockA) { }}Livelock
Section titled “Livelock”Threads keep changing state in response to each other but cannot make progress.
Starvation
Section titled “Starvation”A thread is perpetually denied access to resources and cannot proceed.
Best Practices
Section titled “Best Practices”- Always release locks in a finally block
- Minimize the scope of synchronized blocks
- Prefer higher-level abstractions (
java.util.concurrentclasses) - Avoid holding locks while calling external methods
- Use
volatileonly for simple cases - Avoid nested locks or always acquire locks in a consistent order
- Use timeouts for lock acquisition where possible
Code Examples
Section titled “Code Examples”Using synchronized for Mutual Exclusion
Section titled “Using synchronized for Mutual Exclusion”class Counter { private int count = 0; public synchronized void increment() { count++; } public synchronized int get() { return count; }}Using ReentrantLock
Section titled “Using ReentrantLock”Lock lock = new ReentrantLock();int count = 0;void increment() { lock.lock(); try { count++; } finally { lock.unlock(); }}Using ReadWriteLock
Section titled “Using ReadWriteLock”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(); }}Using volatile
Section titled “Using volatile”volatile boolean running = true;void stop() { running = false; }void run() { while (running) { /* work */ } }