Java Full Course 2026 - Chapter 51: Monitor Locks in Java
This chapter explores concurrency control in Java, focusing on how to resolve race conditions and ensure thread safety using the synchronized keyword. We will examine the mechanics of Monitor Locks (Object Locks), transition from synchronized methods to fine-grained synchronized blocks, implement custom lock objects, and analyze class-level static synchronization.
1. The Concurrency Problem: Critical Sections and Race Conditions
In multi-threaded applications, multiple threads often attempt to access and modify shared resources simultaneously. To understand how to protect these resources, we must define two core concepts:
- Critical Section: A block of code that accesses shared resources and must not be executed by more than one thread at a time.
- Shared Resource: Any variable, object, or file that is accessible to multiple threads (e.g., an instance variable).
The Non-Atomic Nature of count++
A classic example of a race condition occurs when multiple threads increment a shared counter. Although count++ looks like a single operation in Java, it is non-atomic and compiles into three distinct CPU instructions:
1. Read: Read the current value of 'count' from memory (RAM) into a CPU register.
2. Modify: Increment the value in the register by 1.
3. Write: Write the updated value back from the register to memory.
If two threads execute these instructions concurrently without synchronization, their operations can interleave, causing updates to be overwritten. This behavior is called a Race Condition.
2. The Solution: The Synchronized Keyword
The simplest way to prevent race conditions in Java is by using the synchronized keyword. When applied to a method, it ensures that only one thread can execute that method at any given time.
Naive (Unsafe) Approach vs. Thread-Safe Approach
Naive Approach (Prone to Race Conditions)
In this implementation, calling increment() concurrently from multiple threads results in an unpredictable final count (e.g., getting 12,016 instead of the expected 20,000 when two threads increment it 10,000 times each).
class Counter {
public int count = 0;
public void increment() {
count++; // Non-atomic operation
}
}
Thread-Safe Approach (Using synchronized)
By adding the synchronized modifier to the method signature, Java guarantees that the entire execution of increment() behaves atomically relative to other threads.
class Counter {
public int count = 0;
public synchronized void increment() {
count++; // Protected critical section
}
}
With synchronized applied, running two threads that call increment() 10,000 times each will consistently yield exactly 20,000.
3. Thread State Transitions and Lock Acquisition Flow
When a thread attempts to execute a synchronized method, it must acquire an internal lock. If the lock is already held by another thread, the requesting thread's state changes.
Real-Life Analogy: The Locked House
Think of a synchronized block as a house with a single key. When Thread 1 (
T1) arrives, it takes the key, enters the house, and locks the door from the inside. Even ifT1decides to take a nap (e.g., callingThread.sleep()) or gets temporarily distracted (context-switched by the CPU), it still holds the key. Thread 2 (T2) cannot enter the house.T2must wait outside on the porch (theBLOCKEDstate) untilT1finishes its work, exits the house, and returns the key.
Lock Acquisition Workflow
4. Why Do We Need Synchronization? (The Four Pillars)
The synchronized keyword provides four fundamental guarantees for concurrent execution:
- To Protect Shared Data: It prevents multiple threads from corrupting shared memory states.
- To Ensure Atomicity: It groups multiple operations (like Read-Modify-Write) into a single logical unit of work that cannot be split or interleaved by other threads.
- To Ensure Visibility: It guarantees that any write to a synchronized variable is immediately flushed to main memory (RAM), and any subsequent read by another thread is pulled directly from RAM rather than local CPU caches.
- To Prevent Instruction Reordering: The JVM and CPU are restricted from reordering instructions across the boundaries of a synchronized block, preserving the intended execution order.
5. Understanding Monitor Locks (Object Locks)
A common misconception is that the synchronized keyword locks the method itself. In Java, locks belong to objects, not methods.
Every single object created in Java has an implicit, built-in lock called an Intrinsic Lock, Monitor Lock, or Object Lock. This lock is managed automatically by the Java Virtual Machine (JVM).
The "One Object, One Lock" Rule
If an object has multiple synchronized methods, they all share the same monitor lock of that object instance.
class Test {
public synchronized void m1() {
System.out.println("m1 entered by " + Thread.currentThread().getName());
try { Thread.sleep(2000); } catch (Exception e) {}
System.out.println("m1 exiting");
}
public synchronized void m2() {
System.out.println("m2 entered by " + Thread.currentThread().getName());
try { Thread.sleep(2000); } catch (Exception e) {}
System.out.println("m2 exiting");
}
}
If we instantiate a single object of Test:
Test obj = new Test();
And we run two threads:
T1callsobj.m1()T2callsobj.m2()
Even though they are calling different methods, T2 will be blocked until T1 finishes executing m1(). This is because both methods require the monitor lock of the same instance (obj).
Multiple Instances, Multiple Locks
If we create two separate instances of the class:
Test obj1 = new Test();
Test obj2 = new Test();
If T1 calls obj1.m1() and T2 calls obj2.m2(), they will execute concurrently without blocking each other. This is because obj1 and obj2 have separate, independent monitor locks.
6. Synchronized Blocks: Fine-Grained Concurrency
Synchronizing an entire method can introduce significant performance bottlenecks. If a method contains 100 lines of code, but only 2 lines modify a shared resource, locking the entire method forces threads to wait unnecessarily.
To solve this, Java provides Synchronized Blocks. This allows you to lock only the critical section of code.
Syntax
public void increment() {
// Non-critical code here (Executed concurrently by all threads)
synchronized(this) {
// Critical Section (Only one thread can enter at a time)
count++;
}
// More non-critical code here (Executed concurrently)
}
Here, this refers to the current instance of the object, meaning it still uses the object's monitor lock, but only for the duration of the block.
7. Custom Lock Objects: Decoupling Independent Operations
When a class has multiple independent operations that modify different shared resources, synchronizing on this can cause unnecessary blocking.
Real-World Analogy: Bank Deposit and Withdrawal
Imagine a bank class that handles both
deposit()andwithdraw()operations. These operations modify different balances or logs and are completely independent of each other. If we synchronize both methods using thesynchronizedkeyword, a thread performing a deposit will block another thread trying to perform a withdrawal.
To allow these independent operations to run concurrently while remaining thread-safe, we can create Custom Lock Objects.
class Bank {
// Independent dummy objects used solely for their monitor locks
private final Object depositLock = new Object();
private final Object withdrawLock = new Object();
public void deposit() {
synchronized(depositLock) { // Locks only deposit operations
System.out.println("Deposit started by " + Thread.currentThread().getName());
try { Thread.sleep(2000); } catch (Exception e) {}
System.out.println("Deposit finished");
}
}
public void withdraw() {
synchronized(withdrawLock) { // Locks only withdraw operations
System.out.println("Withdraw started by " + Thread.currentThread().getName());
try { Thread.sleep(2000); } catch (Exception e) {}
System.out.println("Withdraw finished");
}
}
}
Execution Behavior
T1callingdeposit()andT2callingdeposit()will block each other (since both needdepositLock).T1callingdeposit()andT2callingwithdraw()will run concurrently (since they acquire different locks:depositLockandwithdrawLock).
8. The "New Object" Synchronization Pitfall (Common Interview Question)
A common mistake when using synchronized blocks is passing a newly instantiated object directly into the synchronized block expression.
The Broken Code
public void m1() {
synchronized(new Object()) { // PITFALL: Creating a new lock every time!
// Critical section
System.out.println(Thread.currentThread().getName() + " entered");
try { Thread.sleep(2000); } catch (Exception e) {}
}
}
Why This Fails
Every time a thread calls m1(), the expression new Object() executes, creating a brand new object in the heap memory.
T1entersm1(), createsObject A, and acquires its lock.T2entersm1(), createsObject B, and acquires its lock.
Since T1 and T2 are locking on completely different objects, no synchronization occurs. Both threads will enter the critical section at the same time, rendering the synchronized block useless.
Rule of Thumb: Always lock on a shared, persistent object reference (e.g., a
private final Object lock = new Object();instance variable orthis).
9. Static Synchronization: Class-Level Locking
When you apply the synchronized keyword to a static method, there is no instance object (this) to lock on. Instead, the lock is acquired on the Class Object (ClassName.class) associated with that class loaded in the JVM.
Static Synchronized Method
class Counter {
public static int count = 0;
public static synchronized void increment() {
count++;
}
}
Equivalent Synchronized Block
The code above is functionally identical to locking on Counter.class inside a synchronized block:
class Counter {
public static int count = 0;
public static void increment() {
synchronized(Counter.class) { // Explicit Class-Level Lock
count++;
}
}
}
Class Lock vs. Instance Lock
It is important to understand that a Class Lock is completely separate from an Instance Lock.
If T1 holds a Class Lock on Counter.class (executing a static synchronized method), another thread T2 can still acquire an Instance Lock on an instance of Counter (executing a non-static synchronized method) at the same time.
10. Conceptual Internal Design of Monitor Locks
To understand how Java manages locks under the hood, we can look at a conceptual model of a monitor lock's internal structure.
Conceptual Lock Structure
An object's monitor lock can be thought of as an internal data structure containing three primary fields:
class MonitorLock {
private Thread ownerThread = null; // The thread currently holding the lock
private boolean isLocked = false; // Lock status flag
private Queue<Thread> waitingQueue = new LinkedList<>(); // Blocked threads
}
Conceptual Lock Acquisition Logic (Pseudo-code)
When a thread attempts to acquire the lock, the JVM executes logic similar to this:
public void acquireLock(Thread currentThread) {
if (!this.isLocked) {
// Lock is free; acquire it
this.isLocked = true;
this.ownerThread = currentThread;
} else {
// Lock is busy; block the thread and add it to the waiting queue
waitingQueue.add(currentThread);
blockThread(currentThread); // Moves thread to BLOCKED state
}
}
11. The Hardware Solution: Compare-And-Swap (CAS)
Looking at the lock acquisition pseudo-code in Section 10, you might notice a classic concurrency problem: the lock acquisition logic itself is a "check-then-act" operation.
if (!this.isLocked) {
// What if T1 and T2 both check this simultaneously,
// see 'false', and both set it to 'true'?
this.isLocked = true;
}
If the lock acquisition code itself is not thread-safe, we run into an infinite loop of needing a lock to protect the lock. To break this cycle, Java relies on hardware-level support.
Compare-And-Swap (CAS)
Modern CPUs provide atomic hardware instructions, most notably Compare-And-Swap (CAS).
A CAS operation takes three arguments:
- A memory location ()
- The expected old value ()
- A new value ()
The CPU updates the memory location to if and only if the current value at is equal to . This entire operation is performed atomically at the hardware level in a single clock cycle.
Java's low-level concurrency libraries (and the JVM's internal monitor lock implementation) use CAS to guarantee that lock acquisition is completely atomic without needing software-level locks.
12. Performance Overhead and Modern Alternatives
While the synchronized keyword is easy to use, it comes with a performance cost:
- Thread Blocking and Context Switching: When a thread is blocked, the operating system moves it out of the CPU register, saves its state, and schedules another thread. This context switch is an expensive operation.
- Unconditional Locking: Threads must wait even if there is no actual contention at runtime.
Modern Alternatives
Because of this overhead, production-grade, high-performance Java applications often avoid heavy use of synchronized. Instead, they use modern alternatives from the java.util.concurrent package:
- ReentrantLocks: Explicit locks that offer advanced features like try-lock (non-blocking lock acquisition attempts), timed lock acquisition, and interruptible lock waits.
- Optimistic Locking: Techniques (like
StampedLockor atomic variables such asAtomicInteger) that assume conflicts are rare. They perform operations without locking and use CAS to detect and retry if a conflict occurs, which is much faster under low contention.
Summary and Key Takeaways
- Critical Sections contain code accessing shared resources that must be executed by only one thread at a time to prevent Race Conditions.
- The
synchronizedkeyword provides an easy way to make critical sections thread-safe by ensuring atomicity, visibility, and ordering. - Locks belong to objects, not methods. Every Java object has an intrinsic monitor lock managed by the JVM.
- One Object, One Lock: If a thread acquires the monitor lock of an object, all other synchronized methods on that same object instance are locked out for other threads.
- Synchronized Blocks provide fine-grained locking, allowing you to lock only the critical lines of code instead of the entire method, which improves performance.
- Custom Lock Objects let you decouple independent operations (like deposits and withdrawals) so they can run concurrently while remaining thread-safe.
- Static Synchronization locks the class's
Classobject (ClassName.class) rather than an individual instance. - Never lock on
new Object()inside a synchronized block, as this creates a new lock instance on every execution, bypassing synchronization entirely. - Compare-And-Swap (CAS) is the atomic hardware-level instruction that allows the JVM to safely implement lock acquisition without software-level race conditions.