Master Java Concurrency: Choose ReentrantLock, ReadWriteLock, StampedLock, or Semaphore

This article explores Java’s advanced lock mechanisms—ReentrantLock, ReentrantReadWriteLock, StampedLock, and Semaphore—detailing their core features, practical code examples, and ideal usage scenarios, helping developers decide which synchronization tool best fits their performance and concurrency requirements.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Master Java Concurrency: Choose ReentrantLock, ReadWriteLock, StampedLock, or Semaphore

In Java, thread synchronization is essential for thread‑safe operations. While synchronized blocks are simple, Java offers more advanced lock mechanisms that provide better control, performance, and flexibility.

This article examines four lock mechanisms—ReentrantLock, ReentrantReadWriteLock, StampedLock, and Semaphore—highlighting their characteristics, typical use cases, and advantages over traditional synchronized blocks.

1. Overview

ReentrantLock

ReentrantReadWriteLock

StampedLock

Semaphore

2.1 ReentrantLock (Flexible Exclusive Lock)

Core features:

Reentrancy: the same thread can acquire the lock multiple times.

Fairness option: can enforce FIFO order to avoid thread starvation.

tryLock(): supports timeout when attempting to acquire the lock.

Condition support: allows creation of multiple wait/notify queues.

Usage example:

public class ReentrantLockDemo {
    private final ReentrantLock lock = new ReentrantLock();

    public void performTask() {
        lock.lock();
        try {
            System.out.printf("%s - executing task%n", Thread.currentThread().getName());
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockDemo rd = new ReentrantLockDemo();
        Runnable task = rd::performTask;
        new Thread(task).start();
        new Thread(task).start();
    }
}

When to use:

When fine‑grained control over the lock is needed (e.g., timeout, interruptible lock).

When synchronized is too restrictive.

2.2 ReentrantReadWriteLock (Optimized for Read‑Heavy Scenarios)

Core features:

Separate read and write locks.

readLock(): shared lock allowing multiple concurrent reads.

writeLock(): exclusive lock permitting only one writer.

Better performance when read operations vastly outnumber writes.

Usage example:

public class ReadWriteLockDemo {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private int value = 0;

    public void write(int newValue) {
        rwLock.writeLock().lock();
        try {
            System.out.printf("%s - writing value: %s%n", Thread.currentThread().getName(), newValue);
            value = newValue;
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public void read() {
        rwLock.readLock().lock();
        try {
            System.out.printf("%s - reading value: %s%n", Thread.currentThread().getName(), value);
            TimeUnit.MILLISECONDS.sleep(600);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public static void main(String[] args) {
        ReadWriteLockDemo rwl = new ReadWriteLockDemo();
        Runnable writer = () -> rwl.write((int) (Math.random() * 100));
        Runnable reader = rwl::read;
        new Thread(writer).start();
        new Thread(reader).start();
        new Thread(reader).start();
    }
}

When to use:

Cache systems where reads dominate and writes are infrequent.

Thread‑safe collections such as ConcurrentHashMap that internally use a similar mechanism.

2.3 StampedLock (High‑Performance Optimistic Lock)

Core features:

Three modes: optimistic read (non‑blocking, checks for conflicts), pessimistic read (blocks writers), and pessimistic write (exclusive).

Generally faster than ReentrantReadWriteLock but more complex to use.

Usage example:

public class StampedLockExample {
    private final StampedLock lock = new StampedLock();
    private int value = 0;

    public void optimisticRead() {
        long stamp = lock.tryOptimisticRead();
        int current = value;
        if (!lock.validate(stamp)) {
            stamp = lock.readLock();
            try {
                current = value;
            } finally {
                lock.unlockRead(stamp);
            }
        }
        System.out.printf("%s - optimistic read: %s%n", Thread.currentThread().getName(), current);
    }

    public void write(int newValue) {
        long stamp = lock.writeLock();
        try {
            System.out.printf("%s - writing value: %s%n", Thread.currentThread().getName(), newValue);
            value = newValue;
        } finally {
            lock.unlockWrite(stamp);
        }
    }

    public static void main(String[] args) {
        StampedLockExample lock = new StampedLockExample();
        Runnable writer = () -> lock.write((int) (Math.random() * 100));
        Runnable reader = lock::optimisticRead;
        new Thread(reader).start();
        new Thread(writer).start();
        new Thread(reader).start();
    }
}

When to use:

High‑performance concurrent structures where reads dominate and conflicts are rare.

2.4 Semaphore (Controlling Concurrent Access)

Core features:

Limits the number of threads that can access a resource.

Non‑reentrant: a thread must release a permit before acquiring another.

Usage example:

public class SemaphoreDemo {
    // Allow at most 2 concurrent threads
    private final Semaphore semaphore = new Semaphore(2);

    public void accessResource() {
        try {
            semaphore.acquire();
            System.out.printf("%s - executing task%n", Thread.currentThread().getName());
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            semaphore.release();
        }
    }

    public static void main(String[] args) {
        SemaphoreDemo sd = new SemaphoreDemo();
        Runnable task = sd::accessResource;
        for (int i = 0; i < 5; i++) {
            new Thread(task).start();
        }
    }
}

When to use:

Resource‑pool management such as database connection pools.

Rate limiting, e.g., throttling API calls.

2.5 Comparison

(A visual comparison chart is omitted for brevity.)

JavaconcurrencySemaphoreLocksReentrantLockReadWriteLockStampedLock
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.