β File 04: Java synchronized Keyword & ReentrantLock
The Java-specific thread safety primitives. Every Java candidate MUST know these.
π What is synchronized? (Simple Explanation)
synchronized is Java's built-in mutex. It's the simplest way to make code thread-safe.
Analogy: Every Java object has an invisible "meeting room" attached to it. synchronized means "only one person can be in this meeting room at a time." The meeting room has a door with a lock, and the key is called a monitor.
// When Thread A enters this block, it grabs the key (monitor).
// Thread B tries to enter β the door is locked β Thread B waits.
// Thread A exits β drops the key β Thread B picks it up and enters.
synchronized (someObject) {
// Only ONE thread can be here at a time
// This is the "critical section"
}π¬ How Does synchronized Work Internally?
The Monitor (Object Header)
Every Java object has a hidden structure called the object header. Part of this header is the mark word, which stores lock information.
βββββββββββββββββββββββββββββββββββββββββββββββ
β Java Object in Memory β
β β
β βββββββββββββββββββββββββββββββββββ β
β β Mark Word (64 bits) β β
β β Contains: β β
β β - Lock state (biased/thin/fat) β β
β β - Thread ID of lock owner β β
β β - Hash code β β
β β - GC age β β
β βββββββββββββββββββββββββββββββββββ β
β βββββββββββββββββββββββββββββββββββ β
β β Class Pointer (Klass*) β β
β βββββββββββββββββββββββββββββββββββ β
β βββββββββββββββββββββββββββββββββββ β
β β Instance Fields β β
β βββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββLock Escalation (The JVM's Optimization Strategy)
The JVM doesn't immediately use a heavy OS lock. It escalates through increasingly expensive mechanisms:
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β Biased β β β Thin Lock β β β Fat Lock β
β Locking β β (CAS-based) β β (OS Mutex) β
β β β β β β
β 0 contentionβ β Light β β Heavy β
β ~0 ns β β contention β β contention β
β β β ~10-20 ns β β ~3000 ns β
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββStage 1: Biased Locking (No Contention)
- If only ONE thread ever accesses the object, the JVM "biases" the lock to that thread
- The thread ID is stored in the mark word
- Subsequent lock/unlock by the SAME thread = zero overhead (just check thread ID)
- Cost: ~0 nanoseconds
Stage 2: Thin Lock / Lightweight Lock (Light Contention)
- If a second thread tries to acquire β bias is revoked
- JVM uses a CAS (compare-and-swap) on the mark word to claim the lock
- The waiting thread spins briefly (user-space, no OS call)
- Cost: ~10-20 nanoseconds
Stage 3: Fat Lock / Heavyweight Monitor (Heavy Contention)
- If spinning fails (another thread holds the lock for too long)
- JVM creates an OS-level monitor (similar to a futex)
- Waiting thread is put to sleep by the OS
- Cost: ~1000-5000 nanoseconds (context switch!)
π» Using synchronized for Tree of Space
Method 1: Synchronized Method (β Too Coarse)
class TreeOfSpace {
// This locks the ENTIRE TreeOfSpace object for EVERY call
// β All threads serialize. Parallelism = 0.
public synchronized boolean lock(String name, int uid) {
// ... all checks and updates ...
}
}Problem:
synchronizedon a method usesthisas the monitor. Only one thread can calllock()at a time, even if they're locking completely independent nodes.
Method 2: Synchronized Block on Shared Object (β Still Too Coarse)
class TreeOfSpace {
private final Object treeLock = new Object();
public boolean lock(String name, int uid) {
synchronized (treeLock) {
// Still serializes everything!
}
}
}Method 3: Per-Node Synchronized (β Fine-Grained)
import java.util.*;
class Node {
String name;
int lockedBy = -1;
Node parent;
List<Node> children = new ArrayList<>();
Set<Node> lockedDescendants = new HashSet<>();
// Each node IS its own monitor object
// synchronized(node) locks THIS specific node
Node(String name, Node parent) {
this.name = name;
this.parent = parent;
}
}
class TreeOfSpace {
Map<String, Node> nodeMap = new HashMap<>();
Node root;
// Build tree... (same as before)
public boolean lock(String name, int uid) {
Node node = nodeMap.get(name);
if (node == null) return false;
// Step 1: Gather path from node to root
List<Node> path = new ArrayList<>();
Node cur = node;
while (cur != null) {
path.add(cur);
cur = cur.parent;
}
// Step 2: Acquire synchronized locks in LEAF-TO-ROOT order
// We use nested synchronized blocks
// Java guarantees: if a thread already holds a monitor,
// it can enter the same synchronized block again (REENTRANT)
return lockHelper(path, 0, node, uid);
}
// Recursive helper to acquire locks one by one
private boolean lockHelper(List<Node> path, int index,
Node target, int uid) {
if (index == path.size()) {
// All locks acquired β perform checks
return performLockChecks(target, uid, path);
}
synchronized (path.get(index)) {
// Lock this node's monitor, then proceed to next
return lockHelper(path, index + 1, target, uid);
}
// Lock auto-releases when synchronized block exits
}
private boolean performLockChecks(Node target, int uid,
List<Node> path) {
// Check 1: Node already locked?
if (target.lockedBy != -1) return false;
// Check 2: Any descendant locked?
if (!target.lockedDescendants.isEmpty()) return false;
// Check 3: Any ancestor locked?
for (int i = 1; i < path.size(); i++) {
if (path.get(i).lockedBy != -1) return false;
}
// All pass β lock it
target.lockedBy = uid;
for (int i = 1; i < path.size(); i++) {
path.get(i).lockedDescendants.add(target);
}
return true;
}
}π ReentrantLock β The Advanced Alternative
ReentrantLock is like synchronized but with superpowers:
| Feature | synchronized | ReentrantLock |
|---|---|---|
| Automatic release | β (exits block) | β (must call unlock() in finally) |
| Try-lock (non-blocking) | β | β
tryLock() |
| Timed lock | β | β
tryLock(5, TimeUnit.SECONDS) |
| Interruptible wait | β | β
lockInterruptibly() |
| Fair ordering | β | β
new ReentrantLock(true) |
| Condition variables | Only wait()/notify() | β
newCondition() |
What Does "Reentrant" Mean?
A reentrant lock allows the same thread to acquire the lock multiple times without deadlocking itself.
ReentrantLock lock = new ReentrantLock();
void methodA() {
lock.lock();
try {
methodB(); // This would DEADLOCK with a non-reentrant lock!
} finally {
lock.unlock();
}
}
void methodB() {
lock.lock(); // Same thread already holds the lock β allowed (reentrant)
try { // Internal counter: 2 (locked twice)
// ...
} finally {
lock.unlock(); // Counter: 1 (still locked once)
}
}
synchronizedis also reentrant! A thread can enter multiplesynchronizedblocks on the same object.
Tree of Space with ReentrantLock:
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
class Node {
String name;
int lockedBy = -1;
Node parent;
List<Node> children = new ArrayList<>();
Set<Node> lockedDescendants = new HashSet<>();
ReentrantLock nodeLock = new ReentrantLock(); // Each node has its own lock
Node(String name, Node parent) {
this.name = name;
this.parent = parent;
}
}
class TreeOfSpace {
Map<String, Node> nodeMap = new HashMap<>();
public boolean lock(String name, int uid) {
Node node = nodeMap.get(name);
if (node == null) return false;
// Gather path
List<Node> path = new ArrayList<>();
Node cur = node;
while (cur != null) {
path.add(cur);
cur = cur.parent;
}
// Acquire ReentrantLocks in leaf-to-root order
for (Node n : path) {
n.nodeLock.lock();
}
try {
// Critical section
if (node.lockedBy != -1) return false;
if (!node.lockedDescendants.isEmpty()) return false;
for (int i = 1; i < path.size(); i++) {
if (path.get(i).lockedBy != -1) return false;
}
// Lock the node
node.lockedBy = uid;
for (int i = 1; i < path.size(); i++) {
path.get(i).lockedDescendants.add(node);
}
return true;
} finally {
// ALWAYS release in finally block!
// Release in REVERSE order (root to leaf)
for (int i = path.size() - 1; i >= 0; i--) {
path.get(i).nodeLock.unlock();
}
}
}
}β‘ volatile in Java vs C++ (Critical Interview Point!)
Java volatile:
volatile boolean isLocked = false;- Guarantees visibility: When Thread A writes
true, Thread B will see it - Provides happens-before: All writes before the volatile write are visible to any thread that reads the volatile variable
- Does NOT guarantee atomicity of compound operations (
x++is still unsafe with volatile!) - Prevents instruction reordering around volatile reads/writes
C/C++ volatile:
volatile bool isLocked = false;- Only prevents compiler optimization (no register caching)
- Does NOT guarantee visibility across CPU cores
- Does NOT emit memory fences
- Much weaker than Java volatile
When to Use Java volatile:
// β
Good: simple flag read by another thread
volatile boolean stopFlag = false;
// Thread 1:
stopFlag = true; // atomic write (single variable)
// Thread 2:
while (!stopFlag) { /* work */ } // will see the update
// β Bad: compound operation
volatile int count = 0;
count++; // NOT thread-safe! Still read-modify-writeπ What to Say to the Interviewer
"In Java,
synchronizedis the built-in mutex mechanism. Every object has a monitor β when a thread enters asynchronizedblock, it acquires that monitor. The JVM optimizes this through lock escalation: biased locking (no contention, ~0 ns), thin lock with CAS (~10 ns), and fat lock with OS mutex (~3000 ns).For the tree problem, I'd give each node its own monitor. I acquire
synchronizedblocks from leaf to root to prevent deadlocks. Siblings in different subtrees don't contend β their paths only overlap at common ancestors.If I need more control (try-lock, timeouts, fairness), I'd use
ReentrantLockinstead. It's explicit lock/unlock withtry-finallyfor safety."