Fundamentals 17 min read

Mastering Java Locks: Reentrant, Synchronized, ReadWrite, and Spin Locks Explained

This article provides a comprehensive guide to Java locking mechanisms, covering ReentrantLock, synchronized, ReadWriteLock, and spin locks, with detailed code examples, performance considerations, common use cases, and best practices to ensure thread safety, avoid deadlocks, and optimize concurrency in multithreaded applications.

FunTester
FunTester
FunTester
Mastering Java Locks: Reentrant, Synchronized, ReadWrite, and Spin Locks Explained

Introduction

Multithreaded programming is essential for leveraging multi‑core processors, but it introduces challenges around thread‑safe access to shared resources. Locks are the core mechanism for coordinating such access in Java.

Reentrant Lock (ReentrantLock)

A ReentrantLock allows the same thread to acquire the lock multiple times without deadlocking, offering flexibility over the built‑in synchronized construct. Typical usage requires explicit lock and unlock calls, usually wrapped in a try‑finally block.

import java.util.concurrent.locks.ReentrantLock;

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

    public void increment() {
        lock.lock(); // acquire lock
        try {
            count++;
        } finally {
            lock.unlock(); // release lock
        }
    }

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

Mutex and synchronized Keyword

The synchronized keyword provides an implicit monitor lock for methods or code blocks, ensuring exclusive access without explicit lock management. It is simpler but less flexible than ReentrantLock.

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

ReadWrite Lock

ReadWriteLock (implemented by ReentrantReadWriteLock) permits multiple concurrent readers while allowing only one writer, improving performance when reads dominate writes.

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

public class SharedResource {
    private int data = 0;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    public int readData() {
        lock.readLock().lock();
        try {
            return data;
        } finally {
            lock.readLock().unlock();
        }
    }

    public void writeData(int newValue) {
        lock.writeLock().lock();
        try {
            data = newValue;
        } finally {
            lock.writeLock().unlock();
        }
    }
}

Spin Lock

A spin lock repeatedly checks a flag until it becomes available, avoiding thread sleep/wakeup overhead. It is suitable for very short critical sections on low‑contention workloads.

import java.util.concurrent.atomic.AtomicBoolean;

public class SpinLock {
    private AtomicBoolean locked = new AtomicBoolean(false);

    public void lock() {
        while (!locked.compareAndSet(false, true)) {
            // spin waiting for lock release
        }
    }

    public void unlock() {
        locked.set(false);
    }
}

Lock Performance and Scalability

Choosing the right lock type balances performance and scalability. Over‑locking can degrade throughput, while inappropriate lock choice can cause contention and bottlenecks. Benchmarking different locks under realistic workloads helps identify the optimal configuration.

Common Lock Use Cases

1. Multithreaded Data Access

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

public class SharedDataAccess {
    private int sharedData = 0;
    private Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try { sharedData++; } finally { lock.unlock(); }
    }

    public int getSharedData() {
        lock.lock();
        try { return sharedData; } finally { lock.unlock(); }
    }
}

2. Cache Management

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

public class CacheManager<K, V> {
    private Map<K, V> cache = new HashMap<>();
    private Lock lock = new ReentrantLock();

    public void put(K key, V value) {
        lock.lock();
        try { cache.put(key, value); } finally { lock.unlock(); }
    }

    public V get(K key) {
        lock.lock();
        try { return cache.get(key); } finally { lock.unlock(); }
    }
}

3. Task Scheduling

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

public class TaskScheduler {
    private Lock lock = new ReentrantLock();

    public void scheduleTask(Runnable task) {
        lock.lock();
        try { task.run(); } finally { lock.unlock(); }
    }
}

4. Resource Pool Management

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

public class ResourceManager {
    private int availableResources;
    private Lock lock = new ReentrantLock();

    public ResourceManager(int initialResources) { availableResources = initialResources; }

    public Resource acquireResource() {
        lock.lock();
        try {
            if (availableResources > 0) {
                availableResources--;
                return new Resource();
            }
            return null;
        } finally { lock.unlock(); }
    }

    public void releaseResource() {
        lock.lock();
        try { availableResources++; } finally { lock.unlock(); }
    }

    private class Resource { /* implementation */ }
}

5. Message Queue

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

public class MessageQueue {
    private Queue<String> queue = new ConcurrentLinkedQueue<>();

    public void sendMessage(String message) { queue.offer(message); }
    public String receiveMessage() { return queue.poll(); }
}

Lock Best Practices

1. Avoid Deadlocks

public class DeadlockExample {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    public void method1() {
        synchronized (lock1) {
            synchronized (lock2) { /* ... */ }
        }
    }

    public void method2() {
        synchronized (lock2) {
            synchronized (lock1) { /* ... */ }
        }
    }
}

Ensure a consistent lock acquisition order or use timeout mechanisms to prevent circular wait conditions.

2. Control Lock Granularity

public class LockGranularityExample {
    private final Object globalLock = new Object();
    private int count = 0;

    public void increment() {
        synchronized (globalLock) { count++; }
    }

    public int getCount() {
        synchronized (globalLock) { return count; }
    }
}

Fine‑grained locks (e.g., read‑write locks) can reduce contention compared to a single global lock.

3. Avoid Excessive Locks

public class TooManyLocksExample {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    public void method1() { synchronized (lock1) { /* ... */ } }
    public void method2() { synchronized (lock2) { /* ... */ } }
    public void method3() { synchronized (lock1) { /* ... */ } }
}

Reusing locks where possible reduces lock‑management overhead and improves throughput.

4. Resource Cleanup

public class ResourceCleanupExample {
    private final Object lock = new Object();
    private List<Resource> resources = new ArrayList<>();

    public void addResource(Resource resource) {
        synchronized (lock) { resources.add(resource); }
    }

    public void closeResources() {
        synchronized (lock) {
            for (Resource r : resources) { r.close(); }
            resources.clear();
        }
    }

    private class Resource { void close() { /* release */ } }
}

5. Concurrency Testing

import java.util.concurrent.CountDownLatch;

public class ConcurrentTestExample {
    private final Object lock = new Object();
    private int count = 0;

    public void increment() { synchronized (lock) { count++; } }
    public int getCount() { synchronized (lock) { return count; } }

    public static void main(String[] args) throws Exception {
        final ConcurrentTestExample ex = new ConcurrentTestExample();
        int threads = 10, incPerThread = 1000;
        CountDownLatch latch = new CountDownLatch(threads);
        for (int i = 0; i < threads; i++) {
            new Thread(() -> {
                for (int j = 0; j < incPerThread; j++) ex.increment();
                latch.countDown();
            }).start();
        }
        latch.await();
        System.out.println("Final count: " + ex.getCount());
    }
}

Conclusion

Locks are indispensable tools for achieving thread safety in Java multithreaded programs. Understanding the characteristics, appropriate use cases, and best‑practice patterns for ReentrantLock, synchronized, ReadWriteLock, and spin locks enables developers to write correct, high‑performance, and scalable concurrent applications.

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.

JavaconcurrencymultithreadingLocksReentrantLockSpinlockReadWriteLock
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

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.