Overview
Java provides multiple mechanisms to control access to shared resources in a concurrent environment:
| Feature | Synchronized Block | StampedLock | VarHandle |
|---|---|---|---|
| Type | Intrinsic Lock | Explicit Lock | Low-Level Memory Access |
| Read/Write Support | No distinction | Yes (Optimistic, Pessimistic) | No lock abstraction |
| Reentrancy | Yes | No | Not applicable |
| Fairness | Not guaranteed | Not guaranteed | Not applicable |
| Best Use Case | Simplicity, legacy | High contention with reads | Atomic field updates |
| Java Version | Since Java 1.0 | Java 8 | Java 9 |
Let’s explore each in detail.
1. synchronized Block
Concept
synchronized is a monitor-based locking mechanism provided since Java 1.0. It allows mutual exclusion, ensuring that only one thread at a time can execute the synchronized block or method.
Syntax
synchronized (lockObject) {
// critical section
}
Or:
public synchronized void method() {
// critical section
}
Characteristics
- Reentrant: A thread can acquire the same lock multiple times.
- Thread-safe: Mutual exclusion.
- Fairness: Not guaranteed.
- Blocking: Threads are blocked if the lock is not available.
- No read-write distinction: All threads are blocked regardless of read/write intent.
Use Case
Simple thread-safe access to shared resources, especially when code needs to be protected at method or block level.
Example
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
2. StampedLock
Concept
StampedLock was introduced in Java 8 to enhance performance, especially in read-heavy concurrent applications.
Unlike ReentrantReadWriteLock, StampedLock provides:
- Read Lock
- Write Lock
- Optimistic Read (non-blocking and fast)
It returns a stamp/token that must be used for unlocking.
Syntax
StampedLock lock = new StampedLock();
long stamp = lock.writeLock();
try {
// write
} finally {
lock.unlockWrite(stamp);
}
Key Features
- Not Reentrant: A thread must not try to acquire a lock it already holds.
- Optimistic Read: Lightweight, allows reads without blocking.
- Pessimistic Locking: Available for both read and write.
- Performance: Better for scenarios with many readers and few writers.
- Complexity: More error-prone due to manual unlocking and stamp management.
Use Case
High-performance applications where reads vastly outnumber writes and the risk of data races is low.
Example
public class StampedLockCounter {
private final StampedLock lock = new StampedLock();
private int count = 0;
public void increment() {
long stamp = lock.writeLock();
try {
count++;
} finally {
lock.unlockWrite(stamp);
}
}
public int getCount() {
long stamp = lock.tryOptimisticRead();
int current = count;
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
current = count;
} finally {
lock.unlockRead(stamp);
}
}
return current;
}
}
3. VarHandle
Concept
VarHandle (introduced in Java 9) provides low-level atomic access to variables, similar to sun.misc.Unsafe but safe and standardized.
VarHandles give fine-grained control over memory visibility and atomic operations like compare-and-set, get-and-set, etc.
Syntax
VarHandle handle = MethodHandles.lookup()
.in(MyClass.class)
.findVarHandle(MyClass.class, "field", int.class);
Then you can use:
handle.getVolatile(myObject); handle.setVolatile(myObject, value); handle.compareAndSet(myObject, expected, newValue);
Key Features
- Low-level, atomic operations.
- Memory fences: Full control over memory visibility.
- Supports volatile, acquire/release semantics.
- Fine-tuned concurrency.
- Non-blocking.
- No locking: Doesn’t prevent other threads from accessing the variable.
Use Case
When building lock-free, non-blocking algorithms and data structures. Replaces some use cases of AtomicInteger, Unsafe, etc.
Example
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
public class VarHandleCounter {
private volatile int count;
private static final VarHandle COUNT_HANDLE;
static {
try {
COUNT_HANDLE = MethodHandles.lookup()
.findVarHandle(VarHandleCounter.class, "count", int.class);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
public void increment() {
int prev;
do {
prev = (int) COUNT_HANDLE.getVolatile(this);
} while (!COUNT_HANDLE.compareAndSet(this, prev, prev + 1));
}
public int getCount() {
return (int) COUNT_HANDLE.getVolatile(this);
}
}
Comparison Table
| Feature | synchronized | StampedLock | VarHandle |
|---|---|---|---|
| API Type | Keyword | Class-based | Class-based |
| Locking Type | Blocking, exclusive | Read/Write, Optimistic | Lock-free |
| Reentrant | Yes | No | Not applicable |
| Read/Write Separation | No | Yes | No |
| Fairness | Not guaranteed | Not guaranteed | Not applicable |
| Optimistic Reads | No | Yes | No |
| Lock Acquisition Cost | High | Moderate | Low |
| Lock Overhead | High under contention | Better scalability | No locking overhead |
| Complexity | Low | Medium | High |
| Suitable for | Simplicity | Read-heavy systems | Fine-grained atomic operations |
| Thread Blocking | Yes | Yes (except optimistic) | No |
| Example Use | Simple shared objects | Caching, statistics, counters | Custom atomic structures |
When to Use What?
| Scenario | Use |
|---|---|
| Simple mutual exclusion | synchronized |
| Read-heavy workloads | StampedLock |
| Low-level concurrency with fine control | VarHandle |
| Need for reentrancy | synchronized |
| Need for optimistic, non-blocking reads | StampedLock |
| Building lock-free data structures | VarHandle |
