Understanding Java Concurrency Locks: synchronized, ReentrantLock, ReadWriteLock, and StampedLock

This article explains Java 8's built‑in lock mechanisms—including synchronized, ReentrantLock, ReadWriteLock, and StampedLock—detailing their characteristics, advanced features, and practical usage through inventory, banking, and order‑processing examples with complete code demonstrations.

The Dominant Programmer
The Dominant Programmer
The Dominant Programmer
Understanding Java Concurrency Locks: synchronized, ReentrantLock, ReadWriteLock, and StampedLock

Introduction

Java 8 provides a rich set of lock mechanisms to support multithreaded concurrent programming, helping developers ensure thread safety and coordinate thread interactions.

1. Built‑in Lock (synchronized)

1.1 Synchronized Methods

public class SynchronizedExample {
    private int count = 0;
    // synchronized instance method
    public synchronized void increment() {
        count++;
    }
    // synchronized static method (locks the class object)
    public static synchronized void staticMethod() {
        // ...
    }
}

1.2 Synchronized Code Blocks

public class SynchronizedBlockExample {
    private final Object lock = new Object();
    private int count = 0;
    public void increment() {
        // lock the explicit object
        synchronized(lock) {
            count++;
        }
    }
}

Characteristics

Mutual exclusion – only one thread can hold the lock at a time.

Re‑entrant – the same thread can acquire the lock repeatedly.

Non‑fair – acquisition order is not guaranteed.

Does not support interruption or timeout while waiting.

2. ReentrantLock

2.1 Basic Usage

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;
    public void increment() {
        lock.lock();   // acquire lock
        try {
            count++;
        } finally {
            lock.unlock(); // release lock
        }
    }
}

2.2 Advanced Features

public class AdvancedReentrantLockExample {
    // fair lock
    private final ReentrantLock lock = new ReentrantLock(true);

    public void tryLockExample() throws InterruptedException {
        // wait up to 1 second to acquire the lock
        if (lock.tryLock(1, TimeUnit.SECONDS)) {
            try {
                // critical section
            } finally {
                lock.unlock();
            }
        } else {
            // handle lock acquisition failure
        }
    }

    public void lockInterruptiblyExample() throws InterruptedException {
        lock.lockInterruptibly(); // interruptible acquisition
        try {
            // critical section
        } finally {
            lock.unlock();
        }
    }
}

Key capabilities:

Re‑entrant, same as synchronized.

Fairness can be configured via the constructor.

Supports tryLock with timeout, lockInterruptibly, and state queries such as isLocked(), isHeldByCurrentThread(), getQueueLength().

2.3 Business Scenario – High‑Concurrency Inventory Management

In a flash‑sale, multiple users attempt to purchase the same product simultaneously. The system must guarantee atomic inventory deduction, prevent overselling, maintain high throughput, and roll back correctly on failure.

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

/**
 * Inventory service using ReentrantLock for thread safety
 */
public class InventoryService {
    private final Map<Long, Integer> inventoryMap = new HashMap<>();
    // fine‑grained lock per product ID
    private final Map<Long, ReentrantLock> lockMap = new ConcurrentHashMap<>();

    public void initInventory(Long itemId, int quantity) {
        inventoryMap.put(itemId, quantity);
        lockMap.putIfAbsent(itemId, new ReentrantLock(true)); // fair lock
    }

    public boolean deductInventory(Long itemId, int quantity) {
        ReentrantLock lock = lockMap.get(itemId);
        if (lock == null) {
            throw new IllegalArgumentException("Item not found");
        }
        try {
            // try to acquire lock within 500 ms to avoid indefinite waiting
            if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
                try {
                    Integer current = inventoryMap.get(itemId);
                    if (current == null || current < quantity) {
                        return false; // insufficient stock
                    }
                    Thread.sleep(10); // simulate business processing
                    inventoryMap.put(itemId, current - quantity);
                    return true;
                } finally {
                    lock.unlock();
                }
            } else {
                System.out.println(Thread.currentThread().getName() + " lock timeout");
                return false;
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.println(Thread.currentThread().getName() + " interrupted");
            return false;
        }
    }

    public int getInventory(Long itemId) {
        return inventoryMap.getOrDefault(itemId, 0);
    }
}

A stress test creates 100 threads that simultaneously attempt to deduct one unit from an initial stock of 500, demonstrating correct final inventory and lock behavior.

3. ReadWriteLock

3.1 Basic Usage

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

public class ReadWriteLockExample {
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private int value;

    public int getValue() {
        rwLock.readLock().lock();
        try {
            return value;
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public void setValue(int newValue) {
        rwLock.writeLock().lock();
        try {
            value = newValue;
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

Characteristics:

Read lock is shared; multiple threads can hold it concurrently.

Write lock is exclusive; only one thread can hold it.

Read‑write mutual exclusion – a write lock blocks both reads and writes.

Both locks are re‑entrant; lock downgrade (write → read) is allowed, upgrade (read → write) is not.

3.2 Business Scenario – Bank Account Management

High‑frequency balance queries (≈100:1 read/write ratio) require fast, consistent reads, while transfers need strict synchronization.

Using ReentrantReadWriteLock per account allows concurrent reads and exclusive writes, avoiding the serialization caused by synchronized or plain ReentrantLock.

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

public class AccountService {
    private final Map<String, Account> accountStore = new ConcurrentHashMap<>();
    private final Map<String, ReadWriteLock> lockStore = new ConcurrentHashMap<>();

    private static class Account {
        String accountId;
        BigDecimal balance;
    }

    public void initAccount(String accountId, BigDecimal initBalance) {
        Account account = new Account();
        account.accountId = accountId;
        account.balance = initBalance;
        accountStore.put(accountId, account);
        lockStore.putIfAbsent(accountId, new ReentrantReadWriteLock(true)); // fair lock
    }

    public BigDecimal queryBalance(String accountId) {
        ReadWriteLock lock = lockStore.get(accountId);
        if (lock == null) throw new IllegalArgumentException("Account not found");
        lock.readLock().lock();
        try {
            Account acc = accountStore.get(accountId);
            Thread.sleep(5); // simulate I/O latency
            return acc.balance;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Query interrupted");
        } finally {
            lock.readLock().unlock();
        }
    }

    public boolean transfer(String from, String to, BigDecimal amount) {
        // lock ordering by account ID to prevent deadlock
        String first = from.compareTo(to) < 0 ? from : to;
        String second = first.equals(from) ? to : from;
        ReadWriteLock lock1 = lockStore.get(first);
        ReadWriteLock lock2 = lockStore.get(second);
        if (lock1 == null || lock2 == null) throw new IllegalArgumentException("Account not found");
        try {
            if (!lock1.writeLock().tryLock(300, TimeUnit.MILLISECONDS)) return false;
            try {
                if (!lock2.writeLock().tryLock(300, TimeUnit.MILLISECONDS)) return false;
                try {
                    Account a1 = accountStore.get(from);
                    Account a2 = accountStore.get(to);
                    if (a1.balance.compareTo(amount) < 0) return false;
                    Thread.sleep(20); // simulate business processing
                    a1.balance = a1.balance.subtract(amount);
                    a2.balance = a2.balance.add(amount);
                    return true;
                } finally {
                    lock2.writeLock().unlock();
                }
            } finally {
                lock1.writeLock().unlock();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Transfer interrupted");
        }
    }
}

4. StampedLock

Basic Usage

import java.util.concurrent.locks.StampedLock;

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

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

    // pessimistic read lock
    public double distanceFromOrigin() {
        long stamp = lock.readLock();
        try {
            return Math.sqrt(x * x + y * y);
        } finally {
            lock.unlockRead(stamp);
        }
    }

    // optimistic read
    public double optimisticDistanceFromOrigin() {
        long stamp = lock.tryOptimisticRead();
        double curX = x, curY = y;
        if (!lock.validate(stamp)) {
            stamp = lock.readLock();
            try {
                curX = x;
                curY = y;
            } finally {
                lock.unlockRead(stamp);
            }
        }
        return Math.sqrt(curX * curX + curY * curY);
    }
}

Features:

Three access modes – write, read, and optimistic read.

Optimistic reads do not block writers, ideal for read‑heavy, write‑light workloads.

All operations return a stamp used for unlocking or validation.

Not re‑entrant and does not support condition variables, but can be converted between modes.

Fairness can be enabled via the constructor (new StampedLock(true)).

5. Condition Variables (Condition)

Basic Usage

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

public class ConditionExample {
    private final Lock lock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition();
    private final Condition notFull = lock.newCondition();
    private final Object[] items = new Object[100];
    private int putPtr, takePtr, count;

    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length) {
                notFull.await(); // wait until not full
            }
            items[putPtr] = x;
            if (++putPtr == items.length) putPtr = 0;
            ++count;
            notEmpty.signal(); // signal that queue is not empty
        } finally {
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await(); // wait until not empty
            }
            Object x = items[takePtr];
            if (++takePtr == items.length) takePtr = 0;
            --count;
            notFull.signal(); // signal that queue is not full
            return x;
        } finally {
            lock.unlock();
        }
    }
}

Business Scenario – E‑commerce Order Processing

A producer‑consumer model handles order creation and fulfillment. Producers place orders into a bounded queue; consumers retrieve orders for payment and inventory deduction. Condition variables allow precise signaling for "queue not full" and "queue not empty" states, supporting batch operations and reducing lock contention.

public class OrderQueue {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();   // queue not full
    private final Condition notEmpty = lock.newCondition();  // queue not empty
    private final Queue<Order> queue = new LinkedList<>();
    private final int capacity;

    public OrderQueue(int capacity) { this.capacity = capacity; }

    public void putBatch(List<Order> orders) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() + orders.size() > capacity) {
                notFull.await();
            }
            queue.addAll(orders);
            System.out.printf("%s produced %d orders, size %d/%d%n",
                Thread.currentThread().getName(), orders.size(), queue.size(), capacity);
            notEmpty.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public List<Order> takeBatch(int batchSize) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() < batchSize) {
                notEmpty.await();
            }
            List<Order> batch = new ArrayList<>(batchSize);
            for (int i = 0; i < batchSize; i++) {
                batch.add(queue.poll());
            }
            System.out.printf("%s consumed %d orders, size %d/%d%n",
                Thread.currentThread().getName(), batchSize, queue.size(), capacity);
            notFull.signalAll();
            return batch;
        } finally {
            lock.unlock();
        }
    }

    // Order class omitted for brevity
}

Key advantages of using multiple Condition objects include precise wake‑up semantics (signalAll for producers or consumers only) and support for interruptible and timed waits, which are not possible with Object.wait()/notify().

Conclusion

The article systematically compares Java's lock primitives, highlighting when to prefer the simplicity of synchronized, the flexibility of ReentrantLock, the read‑write separation of ReadWriteLock, the high‑performance optimistic reads of StampedLock, and the richer coordination offered by Condition variables. Real‑world code samples illustrate design decisions such as fine‑grained locking per business entity, lock fairness trade‑offs, timeout handling, deadlock avoidance via ordered lock acquisition, and batch processing to reduce contention.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaConcurrencymultithreadingLocksReentrantLockConditionReadWriteLockStampedLock
The Dominant Programmer
Written by

The Dominant Programmer

Resources and tutorials for programmers' advanced learning journey. Advanced tracks in Java, Python, and C#. Blog: https://blog.csdn.net/badao_liumang_qizhi

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.