To prevent this, Java provides a mechanism called synchronization, which ensures that only one thread can access a critical section at a time.
Synchronization is primarily achieved using the
synchronized keyword, which relies on an internal locking mechanism known as intrinsic locks or monitor locks.
The "synchronized" Keyword
Thesynchronized keyword is used to control access to a block of code or method so that only one thread can execute it at a time for the same object.
When a thread enters a synchronized section, it acquires a lock. Other threads attempting to enter the same section must wait until the lock is released.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
In this example, only one thread can execute the increment() method at a time for the same Counter object.
How synchronized Works?
Every object in Java has an associated intrinsic (monitor) lock. When a thread enters a synchronized method or block, it attempts to acquire that lock.1. If the lock is available, the thread acquires it immediately and continues execution.
2. If another thread already holds the lock, the current thread enters the BLOCKED state until the lock becomes available.
3. When the synchronized method or block finishes, the lock is released automatically.
Synchronization only works when competing threads synchronize on the same lock object.
class Counter {
private int count = 0;
private final Object lock = new Object();
// Uses the object's intrinsic lock
public void incrementUsingThis() {
synchronized (this) {
count++;
}
}
// Uses a different lock object
public void incrementUsingLock() {
synchronized (lock) {
count++;
}
}
}
If one thread executes incrementUsingThis() while another executes incrementUsingLock(), both methods can run at the same time because they synchronize on different lock objects (this and lock).
Since the shared variable count is being modified concurrently, synchronization does not provide the intended protection, and race conditions can still occur.
Method vs Block-Level Synchronization
Method-Level Synchronization
When you declare a method assynchronized, the lock is acquired before entering the method and released when the method exits.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
Characteristics:
- Locks the entire method- Simple and easy to use
- May reduce performance if overused
Block-Level Synchronization
Instead of locking the entire method, you can synchronize only a specific block of code. This provides more control and better performance.class Counter {
private int count = 0;
public void increment() {
synchronized (this) {
count++;
}
}
}
Here, only the critical section is synchronized, allowing the remaining code to execute without holding the lock.
The object specified inside the
synchronized(...) statement determines which lock is acquired. Different lock objects allow different synchronized sections to execute concurrently.
Advantages:
- Better performance (smaller locked region)- More fine-grained control
- Allows different locks for different resources
Comparison
| Method-Level Synchronization | Block-Level Synchronization |
|---|---|
| Locks the entire method. | Locks only the specific block of code. |
| Easier to implement. | More efficient. |
| Less flexible. | Preferred in real-world applications. |
Intrinsic Locks (Monitor Locks)
Java uses intrinsic locks, also known as monitor locks, to implement synchronization.Every object has a built-in lock that can be used for synchronization. When a thread enters a synchronized block or method, it acquires the object's monitor lock.
Object lock = new Object();
synchronized (lock) {
// Thread holds the object's monitor lock
System.out.println("Inside synchronized block");
}
Intrinsic locks are invoked automatically behind the scenes when you use the synchronized keyword. They come in two forms:
1. Instance Locks (Object-Level)
Used when a synchronized block locks the current instance (synchronized(this)) or when a non-static method is marked synchronized.A. Synchronized Method
The lock is implicitly thethis instance.
public class InstanceMethodCounter {
private int count = 0;
// Locks the specific object instance ('this')
public synchronized void increment() {
count++;
}
public int getCount() { return count; }
}
B. Synchronized Block with this
Explicitly locks the this instance. Useful for locking only a specific portion of a method.public class InstanceBlockCounter {
private int count = 0;
public void increment() {
// Non-thread-safe preparation code can go here unblocked
synchronized (this) {
count++; // Locked on the 'this' instance
}
}
}
C. Private Object Lock (Best Practice)
Locks a dedicated, hidden internal instance variable. Completely prevents external code from hijacking your lock.public class PrivateInstanceLockCounter {
private int count = 0;
// Every object instance gets its own private lock object
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
}
2. Static Locks
These locks protect static (global) variables. Only one thread can hold the lock for that class at a time, regardless of how many instances of the class exist.Used when synchronizing on the
Class object itself (e.g., within a static synchronized method).
A. Static Synchronized Method
The lock is implicitly the Counter.class object.public class StaticMethodCounter {
private static int globalCount = 0;
// Locks the entire StaticMethodCounter.class
public static synchronized void incrementGlobal() {
globalCount++;
}
public static int getGlobalCount() { return globalCount; }
}
B. Synchronized Block with .class Literal
Explicitly locks the Class metadata object. Often used inside non-static methods to modify global data safely.public class StaticClassBlockCounter {
private static int globalCount = 0;
public void incrementGlobal() {
// Locks the Class object across the entire JVM
synchronized (StaticClassBlockCounter.class) {
globalCount++;
}
}
}
C. Static Private Object Lock (Best Practice for Globals)
A hidden, class-level lock object. Safeguards global resources without exposing the .class lock publicly.public class PrivateStaticLockCounter {
private static int globalCount = 0;
// Shared across ALL instances, hidden from outside code
private static final Object staticLock = new Object();
public void incrementGlobal() {
synchronized (staticLock) {
globalCount++;
}
}
}
Key Properties of Intrinsic Locks:
- Each object has one monitor (intrinsic) lock- Only one thread can hold the lock at a time
- Locks are reentrant (same thread can acquire multiple times)
- Automatically released when exiting synchronized block
Reentrancy Example
Intrinsic locks in Java are reentrant, meaning a thread that already owns a lock can acquire the same lock again without blocking.class Example {
public synchronized void methodA() {
methodB();
}
public synchronized void methodB() {
System.out.println("Reentrant lock acquired");
}
}
Here, the same thread can enter methodB() even though it already holds the lock from methodA().
In the next chapter, we will explore the
volatile keyword and understand how Java ensures memory visibility between threads, even when no locking is used.