BLOG POSTS
Java ReentrantLock Example – How to Use Locks

Java ReentrantLock Example – How to Use Locks

ReentrantLock in Java provides an alternative to synchronized blocks with additional flexibility and features like try-timeout, interruptible lock attempts, and fair queuing. While many developers stick with basic synchronization mechanisms, understanding ReentrantLock becomes crucial when building high-performance concurrent applications, implementing custom thread pools, or handling complex synchronization scenarios where you need fine-grained control over locking behavior.

How ReentrantLock Works

ReentrantLock is part of the java.util.concurrent.locks package and implements the Lock interface. Unlike synchronized blocks, ReentrantLock gives you explicit control over lock acquisition and release. The “reentrant” nature means the same thread can acquire the lock multiple times without deadlocking itself – each lock() call must be paired with a corresponding unlock() call.

The lock maintains an internal counter tracking how many times the current thread has acquired it. When a thread calls lock(), the counter increments. When unlock() is called, it decrements. The lock becomes available to other threads only when the counter reaches zero.

import java.util.concurrent.locks.ReentrantLock;

public class BasicReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    private int counter = 0;
    
    public void increment() {
        lock.lock();
        try {
            counter++;
            // Demonstrating reentrancy
            nestedMethod();
        } finally {
            lock.unlock();
        }
    }
    
    private void nestedMethod() {
        lock.lock(); // Same thread acquiring lock again
        try {
            counter += 10;
        } finally {
            lock.unlock();
        }
    }
    
    public int getCounter() {
        lock.lock();
        try {
            return counter;
        } finally {
            lock.unlock();
        }
    }
}

Step-by-Step Implementation Guide

Let’s build a practical thread-safe cache implementation using ReentrantLock with advanced features:

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

public class ThreadSafeCache {
    private final Map cache = new HashMap<>();
    private final ReentrantLock lock = new ReentrantLock(true); // Fair lock
    
    // Basic put operation
    public void put(K key, V value) {
        lock.lock();
        try {
            cache.put(key, value);
            System.out.println("Thread " + Thread.currentThread().getName() + 
                             " added: " + key);
        } finally {
            lock.unlock();
        }
    }
    
    // Get with timeout
    public V get(K key, long timeout, TimeUnit unit) {
        try {
            if (lock.tryLock(timeout, unit)) {
                try {
                    return cache.get(key);
                } finally {
                    lock.unlock();
                }
            } else {
                System.out.println("Failed to acquire lock within timeout");
                return null;
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        }
    }
    
    // Interruptible operation
    public V getInterruptibly(K key) throws InterruptedException {
        lock.lockInterruptibly();
        try {
            // Simulate some processing time
            Thread.sleep(1000);
            return cache.get(key);
        } finally {
            lock.unlock();
        }
    }
    
    // Bulk operation with lock status checking
    public void bulkPut(Map items) {
        lock.lock();
        try {
            System.out.println("Hold count: " + lock.getHoldCount());
            System.out.println("Queue length: " + lock.getQueueLength());
            
            for (Map.Entry entry : items.entrySet()) {
                cache.put(entry.getKey(), entry.getValue());
            }
        } finally {
            lock.unlock();
        }
    }
}

Here’s how to test the implementation:

public class CacheTest {
    public static void main(String[] args) throws InterruptedException {
        ThreadSafeCache cache = new ThreadSafeCache<>();
        
        // Test basic operations
        cache.put("key1", 100);
        
        // Test timeout scenario
        Thread longRunningThread = new Thread(() -> {
            cache.put("key2", 200);
            try {
                Thread.sleep(5000); // Hold lock for 5 seconds
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        
        longRunningThread.start();
        Thread.sleep(100); // Ensure first thread gets the lock
        
        // This should timeout
        Integer result = cache.get("key1", 2, TimeUnit.SECONDS);
        System.out.println("Result with timeout: " + result);
        
        longRunningThread.join();
    }
}

Real-World Use Cases and Examples

ReentrantLock shines in several production scenarios. Here’s a connection pool implementation that demonstrates practical usage:

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class DatabaseConnectionPool {
    private final Queue availableConnections;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition();
    private final int maxSize;
    
    public DatabaseConnectionPool(int maxSize) {
        this.maxSize = maxSize;
        this.availableConnections = new LinkedList<>();
        initializeConnections();
    }
    
    public Connection borrowConnection(long timeout, TimeUnit unit) 
            throws InterruptedException {
        lock.lock();
        try {
            while (availableConnections.isEmpty()) {
                if (!notEmpty.await(timeout, unit)) {
                    return null; // Timeout occurred
                }
            }
            return availableConnections.poll();
        } finally {
            lock.unlock();
        }
    }
    
    public void returnConnection(Connection conn) {
        lock.lock();
        try {
            if (availableConnections.size() < maxSize) {
                availableConnections.offer(conn);
                notEmpty.signal(); // Wake up waiting threads
            }
        } finally {
            lock.unlock();
        }
    }
    
    public int getAvailableConnections() {
        lock.lock();
        try {
            return availableConnections.size();
        } finally {
            lock.unlock();
        }
    }
    
    private void initializeConnections() {
        for (int i = 0; i < maxSize; i++) {
            availableConnections.offer(new Connection("conn-" + i));
        }
    }
    
    // Dummy Connection class for example
    static class Connection {
        private final String id;
        Connection(String id) { this.id = id; }
        public String getId() { return id; }
    }
}

Performance Comparisons and Technical Analysis

Here's a performance comparison between synchronized blocks and ReentrantLock under different scenarios:

Scenario Synchronized Block ReentrantLock ReentrantLock (Fair)
Low contention (1-2 threads) ~15ns per operation ~20ns per operation ~25ns per operation
Medium contention (5-10 threads) ~200ns per operation ~180ns per operation ~220ns per operation
High contention (20+ threads) ~800ns per operation ~600ns per operation ~750ns per operation
Interruptibility Not supported Full support Full support
Timeout operations Not supported tryLock(timeout) tryLock(timeout)

Performance benchmark code you can run:

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

public class LockPerformanceBenchmark {
    private static final int THREADS = 10;
    private static final int OPERATIONS = 1000000;
    
    private int synchronizedCounter = 0;
    private int lockCounter = 0;
    private final ReentrantLock lock = new ReentrantLock();
    
    public void benchmarkSynchronized() throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(THREADS);
        long startTime = System.nanoTime();
        
        for (int i = 0; i < THREADS; i++) {
            new Thread(() -> {
                for (int j = 0; j < OPERATIONS; j++) {
                    synchronized (this) {
                        synchronizedCounter++;
                    }
                }
                latch.countDown();
            }).start();
        }
        
        latch.await();
        long endTime = System.nanoTime();
        System.out.println("Synchronized: " + (endTime - startTime) / 1000000 + "ms");
    }
    
    public void benchmarkReentrantLock() throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(THREADS);
        long startTime = System.nanoTime();
        
        for (int i = 0; i < THREADS; i++) {
            new Thread(() -> {
                for (int j = 0; j < OPERATIONS; j++) {
                    lock.lock();
                    try {
                        lockCounter++;
                    } finally {
                        lock.unlock();
                    }
                }
                latch.countDown();
            }).start();
        }
        
        latch.await();
        long endTime = System.nanoTime();
        System.out.println("ReentrantLock: " + (endTime - startTime) / 1000000 + "ms");
    }
}

Comparison with Alternative Synchronization Mechanisms

Feature synchronized ReentrantLock ReadWriteLock StampedLock
Ease of use Very easy Moderate Complex Very complex
Performance (low contention) Excellent Good Good Excellent
Performance (high contention) Poor Good Excellent Excellent
Timeout support No Yes Yes Yes
Interruptibility No Yes Yes Yes
Fair/unfair modes Unfair only Both Both Unfair only
Condition variables wait/notify only Multiple conditions Multiple conditions No

Best Practices and Common Pitfalls

Here are essential best practices when working with ReentrantLock:

  • Always use try-finally blocks: Never call unlock() outside a finally block, as exceptions can cause deadlocks
  • Match lock/unlock calls: Each lock() must have exactly one corresponding unlock()
  • Use fair locks judiciously: Fair locks prevent starvation but reduce throughput by ~3x
  • Prefer tryLock() with timeout: Helps prevent indefinite blocking in production systems
  • Handle InterruptedException properly: Always restore interrupt status when catching InterruptedException

Common mistakes to avoid:

public class CommonMistakes {
    private final ReentrantLock lock = new ReentrantLock();
    private int value = 0;
    
    // WRONG: unlock() not in finally block
    public void badExample1() {
        lock.lock();
        if (value > 100) {
            return; // Lock never released!
        }
        value++;
        lock.unlock();
    }
    
    // WRONG: Unmatched lock/unlock calls
    public void badExample2() {
        lock.lock();
        lock.lock(); // Acquired twice
        try {
            value++;
        } finally {
            lock.unlock(); // Only released once!
        }
    }
    
    // CORRECT: Proper exception handling
    public void correctExample() {
        try {
            if (lock.tryLock(5, TimeUnit.SECONDS)) {
                try {
                    value++;
                } finally {
                    lock.unlock();
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // Restore interrupt status
        }
    }
}

Advanced debugging techniques:

public class LockDiagnostics {
    private final ReentrantLock lock = new ReentrantLock();
    
    public void diagnosticInfo() {
        System.out.println("Lock held by current thread: " + lock.isHeldByCurrentThread());
        System.out.println("Lock hold count: " + lock.getHoldCount());
        System.out.println("Threads waiting: " + lock.getQueueLength());
        System.out.println("Is fair: " + lock.isFair());
        System.out.println("Has waiters: " + lock.hasQueuedThreads());
    }
    
    // Useful for deadlock detection
    public boolean tryLockWithDiagnostics(long timeout, TimeUnit unit) {
        try {
            boolean acquired = lock.tryLock(timeout, unit);
            if (!acquired) {
                System.out.println("Failed to acquire lock - potential deadlock?");
                diagnosticInfo();
            }
            return acquired;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
}

ReentrantLock provides powerful synchronization capabilities beyond basic synchronized blocks. The key is knowing when the additional complexity is justified - typically in high-performance scenarios, when you need timeout behavior, or when implementing custom synchronization patterns. For simple mutual exclusion, synchronized blocks remain the better choice due to their simplicity and JVM optimizations.

For more detailed information, check the official Java documentation on ReentrantLock and explore the broader java.util.concurrent.locks package for advanced concurrency utilities.



This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.

This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.

Leave a reply

Your email address will not be published. Required fields are marked