🔐 Mastering Locks in Java: The Only Guide You’ll Ever Need

VijayVijay
5 min read

In a multi-threaded environment, Locks are the foundation of safe, efficient concurrency. This guide breaks down each lock type, why and when to use it, how it works from first principles, with Java code and explanation.

1. synchronized (Intrinsic Lock)

✅Why

To achieve mutual exclusion and visibility using intrinsic monitor locks. Simple, built-in to Java, easy to use.

📌 When

Use for basic thread-safe access to shared resources where full mutual exclusion is acceptable.

First Principle

Java associates every object with a monitor. synchronized locks that monitor, ensuring only one thread can execute the block/method at a time.

class Counter {
    private int count = 0;

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

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

Explanation

  • synchronized method acquires the object’s monitor before executing.

  • Automatically releases it once method completes or exception occurs.

🔁 2. ReentrantLock

✅ Why

Gives explicit control over locking/unlocking with additional features like tryLock(), fairness, and conditions.

📌 When

Use when you need:

  • Timed or interruptible locks

  • Fair ordering

  • Partial locking in large methods

First Principle

Unlike synchronized, lock acquisition is manual. Reentrant means the thread can reacquire its own lock.

🧪 Code

import java.util.concurrent.locks.ReentrantLock;

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

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

Explanation

  • lock() acquires, unlock() releases.

  • tryLock() avoids blocking forever.

  • Can be used in try-finally blocks for safe release.

📖 3. ReadWriteLock

✅ Why

Optimizes performance when reads far outnumber writes by allowing multiple concurrent readers.

📌 When

Use when shared resource is read frequently and rarely modified, like config data, cache.

First Principle

Read and write operations have different characteristics:

  • Reads can occur in parallel

  • Writes require exclusivity

🧪 Code

import java.util.concurrent.locks.ReentrantReadWriteLock;

class SharedData {
    private int data = 0;
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

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

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

Explanation

  • readLock() allows multiple threads.

  • writeLock() is exclusive and blocks both readers and writers.

⚡ 4. StampedLock

✅ Why

Enables optimistic reading for faster, lock-free reads with fallback to pessimistic locking.

📌 When

Use in high-concurrency, read-heavy workloads needing best performance.

First Principle

Optimistic read doesn’t block writers, but verifies if data changed using a stamp.

🧪 Code

import java.util.concurrent.locks.StampedLock;

class OptimisticCounter {
    private int count = 0;
    private final StampedLock lock = new StampedLock();

    public int read() {
        long stamp = lock.tryOptimisticRead();
        int result = count;
        if (!lock.validate(stamp)) {
            stamp = lock.readLock();
            try {
                result = count;
            } finally {
                lock.unlockRead(stamp);
            }
        }
        return result;
    }

    public void write(int value) {
        long stamp = lock.writeLock();
        try {
            count = value;
        } finally {
            lock.unlockWrite(stamp);
        }
    }
}

Explanation

  • Optimistic read: fast but not guaranteed

  • Validated with validate()

  • Falls back to real readLock() if needed

5. Semaphore

✅ Why

Controls number of concurrent threads accessing a resource.

📌 When

Use for:

  • Connection pool limits

  • Access control (e.g., 3 threads max)

First Principle

Semaphore uses permits to allow/restrict entry.

🧪 Code

import java.util.concurrent.Semaphore;

class Resource {
    private final Semaphore semaphore = new Semaphore(3); // max 3 permits

    public void accessResource() throws InterruptedException {
        semaphore.acquire();
        try {
            System.out.println("Accessing resource");
            Thread.sleep(1000);
        } finally {
            semaphore.release();
        }
    }
}

Explanation

  • acquire() blocks if no permits

  • release() frees a permit

  • Can be binary (1 permit) = mutex

🧮 6. CountDownLatch

✅ Why

Wait for other threads to complete before proceeding.

📌 When

Use for waiting on multiple services/tasks to finish.

First Principle

Thread waits until internal counter reaches 0 via countDown().

🧪 Code

import java.util.concurrent.CountDownLatch;

class Service implements Runnable {
    private CountDownLatch latch;

    Service(CountDownLatch latch) {
        this.latch = latch;
    }

    public void run() {
        System.out.println("Service started");
        latch.countDown();
    }
}

class App {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);
        new Thread(new Service(latch)).start();
        new Thread(new Service(latch)).start();
        new Thread(new Service(latch)).start();

        latch.await(); // Waits here until latch is 0
        System.out.println("All services started");
    }
}

Explanation

  • await() blocks until counter hits zero

  • One-time use (not resettable)

🔄 7. CyclicBarrier

✅ Why

Makes threads wait for each other to reach a common point.

📌 When

Use for multi-phase computations or simulations.

First Principle

Barrier is broken only when all parties reach it.

🧪 Code

import java.util.concurrent.CyclicBarrier;

class Task implements Runnable {
    private CyclicBarrier barrier;

    Task(CyclicBarrier barrier) {
        this.barrier = barrier;
    }

    public void run() {
        try {
            System.out.println(Thread.currentThread().getName() + " waiting");
            barrier.await();
            System.out.println(Thread.currentThread().getName() + " proceeding");
        } catch (Exception e) { }
    }
}

class App {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(3);
        new Thread(new Task(barrier)).start();
        new Thread(new Task(barrier)).start();
        new Thread(new Task(barrier)).start();
    }
}

Explanation

  • await() blocks until all threads arrive

  • Reusable

🧰 8. LockSupport

✅ Why

Provides low-level thread blocking and waking control.

📌 When

Use in custom thread schedulers, frameworks (like ForkJoinPool).

First Principle

park() blocks thread, unpark() wakes it. No object monitor involved.

🧪 Code

import java.util.concurrent.locks.LockSupport;

class LockSupportExample {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("Thread parking");
            LockSupport.park();
            System.out.println("Thread resumed");
        });
        t.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException ignored) { }

        LockSupport.unpark(t); // Wake the thread
    }
}

Explanation

  • Doesn’t require synchronized block

  • Fine-grained control

  • Used inside AbstractQueuedSynchronizer

📚 Final Summary

Lock TypeWhy Use ItWhen To Use
synchronizedSimple lockingBasic thread safety
ReentrantLockExplicit controlTimed, fair, or interruptible locking
ReadWriteLockParallel readsRead-heavy access
StampedLockOptimistic readsHigh-performance, read-mostly systems
SemaphoreLimit access to resourcesDB pool, rate-limiting
CountDownLatchWait for threads to finishInit tasks, parallel startup
CyclicBarrierSync thread stepsRepeating, phased algorithms
LockSupportLow-level controlCustom frameworks, blocking policies

This article is all you need to understand Java Locks from first principles, with real-world examples and decisions for correct usage.

0
Subscribe to my newsletter

Read articles from Vijay directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Vijay
Vijay