Lock is a synchronization mechanism that provides exclusive access to critical sections of code in multithreaded applications. According to Oracle, 2024, the Lock interface provides more flexible synchronization control compared to traditional synchronized blocks, including timed lock attempts and support for multiple wait queues.
Key Takeaways
Lock is an interface from the java.util.concurrent.locks package that provides explicit lock and unlock operations for synchronizing data access. Unlike synchronized, Lock gives the developer full control over the locking mechanism.
The Lock interface was introduced in Java 5 as an alternative to the built-in synchronized mechanism. The main methods are lock, unlock, tryLock, and lockInterruptibly. Locks help organize safe data access in a multithreaded environment, preventing race conditions and data corruption.
The main advantage of Lock over synchronized is flexibility. The developer can attempt to acquire a lock with a timeout, check its availability without blocking, or organize multiple wait queues with different priorities.
Before the Lock interface appeared in Java 5, the only synchronization method was synchronized, which suffered from limitations: no timeouts, no interruptible waiting, and a single queue. Doug Lea designed the java.util.concurrent package, including Lock as a fundamental building block.
A lock manages access through an internal state flag and a wait queue. When a thread calls lock(), the mechanism checks if the lock is free and either acquires it or places the thread in the queue until released.
At the core of any lock lies an atomic compare-and-swap (CAS) operation. When lock() is called, the thread attempts to atomically set the busy flag. If the flag is already set, the thread blocks. On unlock(), the flag is cleared and one waiting thread is awakened.
import java.util.concurrent.locks.ReentrantLock
val lock = ReentrantLock()
fun performTask() {
lock.lock()
try {
// critical section
println("Thread ${Thread.currentThread().name} is working")
} finally {
lock.unlock()
}
}
ReentrantLock internally uses a doubly-linked list (CLH lock queue) where each waiting thread is represented by a node. When the lock is released, the head node of the queue is awakened. Fair mode guarantees FIFO ordering, while unfair mode allows a new thread to acquire the lock ahead of waiting ones to improve throughput.
In the modern Java ecosystem, there are several lock implementations, each optimized for specific scenarios. Choosing the right lock directly impacts the performance and reliability of a multithreaded application.
ReentrantLock is the basic and most commonly used Lock implementation. It supports reentrant acquisition by the same thread: if a thread already holds the lock, calling lock() again does not block it. This prevents deadlock in recursive calls.
ReadWriteLock separates locks into two modes: read and write. Multiple threads can hold the read lock simultaneously, but write access requires exclusive access. This significantly improves performance under frequent reads and rare writes.
StampedLock is the newest implementation, introduced in Java 8. It supports three modes: write, read, and optimistic read. Optimistic read does not block other threads and validates data after reading, providing a 10-20% performance boost over ReadWriteLock.
| Lock | Java Version | Modes | Performance |
|---|---|---|---|
| ReentrantLock | Java 5 | exclusive | high |
| ReadWriteLock | Java 5 | read + write | medium |
| StampedLock | Java 8 | read + write + optimistic | very high |
ReentrantLock is the most popular Lock implementation, providing several features unavailable in synchronized. Understanding its characteristics is essential for effective multithreading work.
The ReentrantLock constructor accepts a fair parameter. When true, the lock guarantees FIFO ordering; when false, a new thread may acquire the lock before waiting ones. Fair mode prevents starvation but reduces throughput by 10-20% due to the overhead of queue maintenance.
Unlike synchronized, ReentrantLock supports tryLock with a timeout. If the lock cannot be acquired within the specified time, the thread continues execution instead of blocking indefinitely. The lockInterruptibly method allows interrupting a waiting thread via Thread.interrupt().
val lock = ReentrantLock()
fun tryTask() {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
println("Lock acquired")
} finally {
lock.unlock()
}
} else {
println("Failed to acquire lock")
}
}
ReentrantLock supports multiple condition variables via the newCondition() method. Each Condition has its own wait queue, enabling complex wakeup scenarios. The await() and signal() methods replaced wait() and notify() from synchronized blocks, but with support for multiple queues.
ReadWriteLock and StampedLock address the optimization of access when reads predominate over writes. They are significantly more efficient than ReentrantLock in scenarios where reading occurs more often than writing.
The ReadWriteLock interface contains two methods: readLock() and writeLock(). The read lock can be held by multiple threads simultaneously, while the write lock is exclusive. A typical example is a thread-safe cache: many threads read data while only one periodically updates it.
class SafeCache<K, V> {
private val map = mutableMapOf<K, V>()
private val rwLock = ReentrantReadWriteLock()
fun get(key: K): V? {
rwLock.readLock().lock()
return try { map[key] } finally { rwLock.readLock().unlock() }
}
fun put(key: K, value: V) {
rwLock.writeLock().lock()
return try { map[key] = value } finally { rwLock.writeLock().unlock() }
}
}
StampedLock adds a third mode — tryOptimisticRead. This mode does not block other threads but merely records a state stamp. After reading, the developer calls validate(stamp) to check if data changed during the read. If data changed, the operation must be retried.
In mobile applications, locks are used to coordinate access to shared data between threads. However, their use requires special caution due to limited device resources and the need to maintain UI responsiveness.
On Android, ReentrantLock is useful when working with Room, caches, and files. It is important to remember: never acquire a lock on the main thread. For asynchronous code, coroutines and Mutex from kotlinx.coroutines are preferable, as they suspend the coroutine instead of blocking the thread.
In iOS, the standard NSLock is used less frequently — developers prefer DispatchQueue with barrier flags or os_unfair_lock. Swift 5.7+ provides modern synchronization mechanisms through actors, which automatically protect state.
import Foundation
actor DataStore {
private var items: [String] = []
func add(_ item: String) {
items.append(item)
}
func getAll() -> [String] {
items
}
}
To avoid deadlocks, follow a consistent lock ordering across the entire project. Use tryLock with a timeout instead of lock() wherever prolonged blocking is possible. Consider using Lock-Free algorithms (AtomicReference, ConcurrentHashMap) instead of traditional locks.
Using Lock requires discipline and adherence to several rules that prevent deadlocks and performance degradation. These practices have been developed by the Java community over 20 years of using the java.util.concurrent package.
The most important pattern is lock in finally. Regardless of whether the critical section completes successfully or throws an exception, the lock must be released. This ensures that other threads are not blocked forever due to a single error. In Kotlin, this pattern is elegantly solved through the withLock extension.
The critical section should be as short as possible. Never perform I/O, network requests, or lengthy computations inside a lock. If you need to read data from a server, fetch it first, then acquire the lock only to update the shared state. This reduces contention and improves system throughput.
To prevent deadlocks when working with multiple locks, establish a global lock ordering across the entire project. If lockA is acquired first, then lockB — any reverse sequence must be prohibited by code review rules. Use static analyzers such as SpotBugs and IntelliJ Inspections for automatic verification.
Frequently Asked Questions
Lock is an explicit interface with timeout and interruptible waiting support. synchronized automatically acquires and releases the monitor but does not allow using tryLock, lockInterruptibly, or multiple Conditions. Lock is more flexible but requires manual release in finally.
A fair lock guarantees FIFO ordering: the thread that has been waiting the longest receives the lock first. An unfair lock may grant access to a new thread ahead of waiting ones, which increases throughput but may cause starvation for waiting threads.
Follow a fixed order for acquiring all locks, use tryLock with a timeout instead of unconditional lock(), and minimize the number of simultaneously held locks. Using Lock-Free data structures also reduces the risk of deadlock.
Condition is the wait/notify analog for Lock, allowing multiple independent wait queues. Each newCondition() call creates a separate queue, providing more precise control over thread wakeup compared to the single queue in synchronized.
For Android with coroutines, use Mutex from kotlinx.coroutines — it suspends the coroutine instead of blocking the thread. For iOS with Swift 5.7+, actors are preferred as they automatically synchronize state access. Reserve ReentrantLock for legacy code and low-level scenarios.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also