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

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 permitsrelease()
frees a permitCan 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 zeroOne-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 arriveReusable
🧰 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 Type | Why Use It | When To Use |
synchronized | Simple locking | Basic thread safety |
ReentrantLock | Explicit control | Timed, fair, or interruptible locking |
ReadWriteLock | Parallel reads | Read-heavy access |
StampedLock | Optimistic reads | High-performance, read-mostly systems |
Semaphore | Limit access to resources | DB pool, rate-limiting |
CountDownLatch | Wait for threads to finish | Init tasks, parallel startup |
CyclicBarrier | Sync thread steps | Repeating, phased algorithms |
LockSupport | Low-level control | Custom 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.
Subscribe to my newsletter
Read articles from Vijay directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
