Use Cases Where Shared Lock / Exclusive Lock Are Useful
Shared and exclusive locks are essential for managing concurrent access to shared resources, ensuring correctness and consistency while maximizing performance. Here are key scenarios:
- Read-Write Access Control:
- Use shared locks (read locks) when multiple threads need to read data concurrently.
- Use exclusive locks (write locks) when a thread needs to modify data, ensuring no other thread can read or write during the modification.
- Database Systems:
- Shared locks are used for read-only transactions, allowing multiple readers.
- Exclusive locks are used for write operations, preventing concurrent reads or writes to ensure consistency.
- Deduplication in Storage:
- Writers (shared lock) may upload the same file concurrently, while deleters (exclusive lock) need to ensure the file is not being written when deleting.
- Cache or Memory Management:
- Shared locks allow threads to read a cached object concurrently.
- Exclusive locks allow one thread to update or invalidate the cache entry.
Implementation in Java Using Compare-And-Set and a Mask
A shared lock and exclusive lock system can be implemented using compareAndSet with a mask to differentiate shared and exclusive states. The lock state is stored as an integer, where:
- The lower 16 bits track the exclusive lock count.
- The upper 16 bits track the shared lock count.
Why Use a Mask (e.g., SHARED_UNIT)?
Instead of incrementing by 1, we increment by a constant (SHARED_UNIT = 1 << 16 = 0x00010000) for shared locks. This allows the upper 16 bits to track shared locks independently of exclusive locks in the lower 16 bits. This separation avoids conflicts between shared and exclusive locks.
Java Implementation
Here’s a simplified implementation of a shared-exclusive lock:
public class SharedExclusiveLock {
private static final int SHARED_UNIT = (1 << 16); // Increment for shared locks
private static final int MAX_SHARED = 0xFFFF0000; // Maximum shared count
private static final int MAX_EXCLUSIVE = 0x0000FFFF; // Maximum exclusive count
private final AtomicInteger state = new AtomicInteger(0);
// Acquire shared lock
public boolean acquireShared() {
for (;;) {
int current = state.get();
int sharedCount = current & MAX_SHARED;
// Fail if exclusive lock is held
if ((current & MAX_EXCLUSIVE) > 0) return false;
// Increment shared count
if (sharedCount < MAX_SHARED) {
if (state.compareAndSet(current, current + SHARED_UNIT)) {
return true;
}
} else {
throw new IllegalStateException("Too many shared locks");
}
}
}
// Release shared lock
public void releaseShared() {
for (;;) {
int current = state.get();
if (state.compareAndSet(current, current - SHARED_UNIT)) {
return;
}
}
}
// Acquire exclusive lock
public boolean acquireExclusive() {
for (;;) {
int current = state.get();
if (current != 0) return false; // Shared or exclusive lock is held
if (state.compareAndSet(0, 1)) {
return true;
}
}
}
// Release exclusive lock
public void releaseExclusive() {
if (!state.compareAndSet(1, 0)) {
throw new IllegalStateException("Invalid exclusive lock release");
}
}
}Key Points:
- Shared and exclusive locks operate on separate portions of the
statevariable. - The
SHARED_UNITensures increments for shared locks affect only the upper 16 bits. - Exclusive locks use the lower 16 bits, allowing independent tracking.
How It Works Under the Hood (CMPXCHG, LDXR/STXR)
Atomicity with CPU Primitives
The compareAndSet method relies on atomic compare-and-swap (CAS) instructions provided by the CPU. These instructions ensure atomicity without requiring OS-level synchronization.
CMPXCHG (x86)
- Instruction:
CMPXCHG(Compare and Exchange) - Behavior:
- Compare the value in a memory location with an expected value.
- If they match, replace the value with a new one.
- If not, return the current value.
- Usage in Java:
- The JVM emits the
CMPXCHGinstruction forcompareAndSetoperations.
- The JVM emits the
Example:
lock cmpxchg [mem], regLDXR/STXR (ARM)
- ARM uses Load-Exclusive (LDXR) and Store-Exclusive (STXR) instructions:
LDXR: Loads the value from memory and marks it for exclusive access.STXR: Stores a new value only if the memory location hasn’t been modified since the load.
Example:
ldxr w0, [mem] // Load exclusive
cmp w0, expected // Compare with expected
b.ne fail // Branch if not equal
stxr w1, new_value, [mem] // Store exclusive
cbnz w1, retry // Retry if store failedWhy CAS Is Fast:
- Entirely handled in hardware (no syscall overhead).
- Atomicity ensures no need for explicit locking.
- Used extensively in lock-free data structures and synchronization primitives.