data-driven-docs

Living documentation for evolving technologies

View on GitHub

Locks and Conditions

Advanced Java 5+ Est. Time: 25min


Table of Contents


Overview

The java.util.concurrent.locks package provides more flexible and powerful locking mechanisms than the synchronized keyword.

Key Interfaces:

Key Implementations:

Advantages over synchronized:

Back to top


ReentrantLock

ReentrantLock is a reentrant mutual exclusion lock with extended capabilities.

Basic Usage

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

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

    public void increment() {
        lock.lock();  // Acquire lock
        try {
            count++;
        } finally {
            lock.unlock();  // ALWAYS unlock in finally!
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

Critical Pattern:

lock.lock();
try {
    // Critical section
} finally {
    lock.unlock();  // Ensures unlock even if exception thrown
}

tryLock() - Non-Blocking Acquisition

public class BankAccount {
    private final Lock lock = new ReentrantLock();
    private int balance = 1000;

    public boolean withdraw(int amount) {
        if (lock.tryLock()) {  // Try to acquire without blocking
            try {
                if (balance >= amount) {
                    balance -= amount;
                    return true;
                }
                return false;
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("Could not acquire lock, try later");
            return false;
        }
    }
}

tryLock(timeout) - Timed Acquisition

import java.util.concurrent.TimeUnit;

public void transfer(Account from, Account to, int amount)
        throws InterruptedException {
    // Try to acquire both locks with timeout
    if (from.getLock().tryLock(1, TimeUnit.SECONDS)) {
        try {
            if (to.getLock().tryLock(1, TimeUnit.SECONDS)) {
                try {
                    from.debit(amount);
                    to.credit(amount);
                } finally {
                    to.getLock().unlock();
                }
            } else {
                System.out.println("Timeout acquiring destination lock");
            }
        } finally {
            from.getLock().unlock();
        }
    } else {
        System.out.println("Timeout acquiring source lock");
    }
}

lockInterruptibly() - Interruptible Lock

public void processWithInterruption() throws InterruptedException {
    lock.lockInterruptibly();  // Can be interrupted while waiting
    try {
        // Process data
        longRunningOperation();
    } finally {
        lock.unlock();
    }
}

// In another thread:
processingThread.interrupt();  // Will throw InterruptedException

Fair vs Unfair Locks

// Unfair lock (default) - better performance
Lock unfairLock = new ReentrantLock();

// Fair lock - FIFO ordering, prevents starvation
Lock fairLock = new ReentrantLock(true);

Fair Lock:

Unfair Lock:

Lock Information Methods

ReentrantLock lock = new ReentrantLock();

lock.lock();
try {
    boolean held = lock.isHeldByCurrentThread();  // true
    int holdCount = lock.getHoldCount();  // 1 (or more if nested)
    boolean locked = lock.isLocked();  // true
    boolean fair = lock.isFair();  // false (default)
} finally {
    lock.unlock();
}

Back to top


Lock vs synchronized

Feature synchronized ReentrantLock
Syntax Language keyword Class with methods
Acquire Lock Automatic lock()
Release Lock Automatic unlock() (in finally!)
Try Lock ❌ No tryLock()
Timed Lock ❌ No tryLock(time, unit)
Interruptible ❌ No lockInterruptibly()
Fairness ❌ No ✅ Optional
Multiple Conditions ❌ One (implicit) ✅ Multiple
Lock Status ❌ No query isLocked(), getHoldCount()
Deadlock Detection ❌ No ✅ Can be detected
Performance Slightly better Slightly worse

When to use synchronized:

When to use ReentrantLock:

Back to top


Condition Interface

Condition provides await/signal mechanisms similar to wait/notify, but more powerful.

Basic Usage

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class BoundedBuffer<T> {
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    private final T[] buffer;
    private int count, putIndex, takeIndex;

    @SuppressWarnings("unchecked")
    public BoundedBuffer(int capacity) {
        buffer = (T[]) new Object[capacity];
    }

    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            while (count == buffer.length) {
                notFull.await();  // Wait until not full
            }
            buffer[putIndex] = item;
            putIndex = (putIndex + 1) % buffer.length;
            count++;
            notEmpty.signal();  // Signal waiting consumers
        } finally {
            lock.unlock();
        }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await();  // Wait until not empty
            }
            T item = buffer[takeIndex];
            takeIndex = (takeIndex + 1) % buffer.length;
            count--;
            notFull.signal();  // Signal waiting producers
            return item;
        } finally {
            lock.unlock();
        }
    }
}

Condition vs wait/notify

Feature wait/notify Condition
Conditions per Object 1 (implicit) Multiple
Signal Specific ❌ No ✅ Yes (different conditions)
Spurious Wakeups ⚠️ Possible ⚠️ Possible (use while loop)
Timed Wait ✅ Yes await(time, unit)
Uninterruptible Wait ❌ No awaitUninterruptibly()
Wait Until Time ❌ No awaitUntil(deadline)

Multiple Conditions Example

public class PriorityQueue<T> {
    private final Lock lock = new ReentrantLock();
    private final Condition highPriorityAvailable = lock.newCondition();
    private final Condition lowPriorityAvailable = lock.newCondition();

    private Queue<T> highPriority = new LinkedList<>();
    private Queue<T> lowPriority = new LinkedList<>();

    public void addHighPriority(T item) {
        lock.lock();
        try {
            highPriority.add(item);
            highPriorityAvailable.signal();  // Signal high-priority waiters
        } finally {
            lock.unlock();
        }
    }

    public void addLowPriority(T item) {
        lock.lock();
        try {
            lowPriority.add(item);
            lowPriorityAvailable.signal();  // Signal low-priority waiters
        } finally {
            lock.unlock();
        }
    }

    public T takeHighPriority() throws InterruptedException {
        lock.lock();
        try {
            while (highPriority.isEmpty()) {
                highPriorityAvailable.await();
            }
            return highPriority.remove();
        } finally {
            lock.unlock();
        }
    }
}

Back to top


ReadWriteLock

ReadWriteLock maintains a pair of locks: one for read-only operations, one for writing.

Concept:

Basic Usage

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class CachedData {
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();

    private Map<String, String> cache = new HashMap<>();

    // Multiple threads can read simultaneously
    public String get(String key) {
        readLock.lock();
        try {
            return cache.get(key);
        } finally {
            readLock.unlock();
        }
    }

    // Only one thread can write
    public void put(String key, String value) {
        writeLock.lock();
        try {
            cache.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }

    // No reads or writes allowed during clear
    public void clear() {
        writeLock.lock();
        try {
            cache.clear();
        } finally {
            writeLock.unlock();
        }
    }
}

When to Use ReadWriteLock

Good Use Cases:

Poor Use Cases:

Fairness and Ordering

// Unfair (default) - writers can barge ahead
ReadWriteLock unfair = new ReentrantReadWriteLock();

// Fair - FIFO ordering
ReadWriteLock fair = new ReentrantReadWriteLock(true);

Back to top


StampedLock

StampedLock (Java 8+) provides an optimistic reading mechanism for better performance.

Three Modes:

  1. Writing: Exclusive access
  2. Reading: Shared access (like ReadWriteLock)
  3. Optimistic Reading: Non-blocking read (unique to StampedLock)

Basic Usage

import java.util.concurrent.locks.StampedLock;

public class Point {
    private final StampedLock lock = new StampedLock();
    private double x, y;

    // Write lock (exclusive)
    public void move(double deltaX, double deltaY) {
        long stamp = lock.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            lock.unlockWrite(stamp);
        }
    }

    // Optimistic read (non-blocking!)
    public double distanceFromOrigin() {
        long stamp = lock.tryOptimisticRead();  // Non-blocking!
        double currentX = x;  // Read without lock
        double currentY = y;

        if (!lock.validate(stamp)) {  // Check if values changed
            // Data changed, acquire read lock
            stamp = lock.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                lock.unlockRead(stamp);
            }
        }

        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    // Pessimistic read lock
    public double getX() {
        long stamp = lock.readLock();
        try {
            return x;
        } finally {
            lock.unlockRead(stamp);
        }
    }
}

Optimistic Reading Flow

Optimistic Reading Flow

Lock Upgrade/Downgrade

public void updateConditionally(double newX, double newY) {
    long stamp = lock.readLock();  // Start with read lock
    try {
        while (x == 0.0 && y == 0.0) {
            long writeStamp = lock.tryConvertToWriteLock(stamp);
            if (writeStamp != 0L) {  // Upgrade succeeded
                stamp = writeStamp;
                x = newX;
                y = newY;
                break;
            } else {  // Upgrade failed
                lock.unlockRead(stamp);
                stamp = lock.writeLock();  // Acquire write lock explicitly
            }
        }
    } finally {
        lock.unlock(stamp);
    }
}

StampedLock vs ReadWriteLock

Feature ReadWriteLock StampedLock
Optimistic Read ❌ No ✅ Yes
Non-blocking Reads ❌ No ✅ Yes (optimistic)
Reentrancy ✅ Yes ❌ No
Condition Support ✅ Yes ❌ No
Performance Good Better (with optimistic reads)
Complexity Lower Higher

When to use StampedLock:

Back to top


Best Practices

1. Always Unlock in finally

// BAD: Unlock might not happen
lock.lock();
criticalSection();
lock.unlock();  // If criticalSection() throws, lock never released!

// GOOD: Always unlock in finally
lock.lock();
try {
    criticalSection();
} finally {
    lock.unlock();  // Always executes
}

2. Use try-with-resources Pattern (Custom)

// Create a helper class for cleaner syntax
public class LockResource implements AutoCloseable {
    private final Lock lock;

    public LockResource(Lock lock) {
        this.lock = lock;
        lock.lock();
    }

    @Override
    public void close() {
        lock.unlock();
    }
}

// Usage
try (LockResource ignored = new LockResource(lock)) {
    // Critical section
}  // Auto-unlock

3. Prefer tryLock with Timeout

// GOOD: Avoid indefinite blocking
if (lock.tryLock(10, TimeUnit.SECONDS)) {
    try {
        // Critical section
    } finally {
        lock.unlock();
    }
} else {
    // Handle timeout (log, retry, fail gracefully)
    throw new TimeoutException("Could not acquire lock");
}

4. Use Condition for Complex Wait Scenarios

// Multiple conditions for different states
Lock lock = new ReentrantLock();
Condition dataReady = lock.newCondition();
Condition spaceAvailable = lock.newCondition();

// Much clearer than single wait/notify

5. Document Lock Ordering

/**
 * Lock ordering: Always acquire locks in this order to prevent deadlock:
 * 1. accountLock
 * 2. transactionLock
 * 3. auditLock
 */
public class BankingSystem {
    private final Lock accountLock = new ReentrantLock();
    private final Lock transactionLock = new ReentrantLock();
    private final Lock auditLock = new ReentrantLock();
}

Back to top


Common Pitfalls

❌ 1. Forgetting to Unlock

// WRONG: If exception occurs, lock never released
lock.lock();
riskyOperation();
lock.unlock();  // Never reached if exception thrown!

// CORRECT:
lock.lock();
try {
    riskyOperation();
} finally {
    lock.unlock();
}

❌ 2. Unlocking Without Holding Lock

// WRONG: IllegalMonitorStateException
lock.unlock();  // Don't hold lock!

// WRONG: Unbalanced lock/unlock
lock.lock();
lock.unlock();
lock.unlock();  // IllegalMonitorStateException

❌ 3. Using StampedLock as Reentrant

// WRONG: StampedLock is NOT reentrant!
StampedLock lock = new StampedLock();
long stamp = lock.writeLock();
long stamp2 = lock.writeLock();  // DEADLOCK! Can't reacquire

❌ 4. Holding Lock During I/O

// BAD: Holding lock during slow I/O
lock.lock();
try {
    data = fetchDataFromNetwork();  // Slow! Others blocked
    process(data);
} finally {
    lock.unlock();
}

// GOOD: Fetch outside lock
data = fetchDataFromNetwork();  // No lock
lock.lock();
try {
    process(data);  // Only protect critical section
} finally {
    lock.unlock();
}

❌ 5. Not Checking tryLock Result

// WRONG: Assuming lock acquired
lock.tryLock();
criticalSection();  // Might execute without lock!
lock.unlock();

// CORRECT: Check return value
if (lock.tryLock()) {
    try {
        criticalSection();
    } finally {
        lock.unlock();
    }
}

Back to top


Ref.

Official Documentation:

Books:

Guides:

Related Topics:


Get Started | Java Concurrency | Java 8