Lock: What It Is, Types of Locks and Usage in Synchronization

Author: IT Sectr Published: 2026-03-19 Reading time: 8 min

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 for explicit lock management in Java.
  • ReentrantLock is a basic implementation supporting reentrant acquisition by the same thread.
  • ReadWriteLock separates read and write locks for better performance.
  • Deadlock is the main risk when using multiple locks simultaneously.
  • Unlike synchronized, Lock supports timeouts and interruptible waiting.

What is Lock?

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.

Definition and Role in Synchronization

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.

History and Evolution

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.

How Does a Lock Work?

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.

Atomic Acquisition and Release

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.

kotlin
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()
    }
}

Wait Queue and Wakeup

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.

Main Types of Locks

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

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.

ReentrantReadWriteLock

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

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.

LockJava VersionModesPerformance
ReentrantLockJava 5exclusivehigh
ReadWriteLockJava 5read + writemedium
StampedLockJava 8read + write + optimisticvery high

ReentrantLock and Its Features

ReentrantLock is the most popular Lock implementation, providing several features unavailable in synchronized. Understanding its characteristics is essential for effective multithreading work.

Lock Fairness

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.

Timeouts and Interruptible Waiting

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().

kotlin
val lock = ReentrantLock()

fun tryTask() {
    if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
        try {
            println("Lock acquired")
        } finally {
            lock.unlock()
        }
    } else {
        println("Failed to acquire lock")
    }
}

Conditions

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

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.

ReadWriteLock in Practice

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.

kotlin
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 and Optimistic Reading

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.

Locks in Mobile Development

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.

Locks in Android (Kotlin)

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.

Locks in iOS (Swift)

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.

swift
import Foundation

actor DataStore {
    private var items: [String] = []

    func add(_ item: String) {
        items.append(item)
    }

    func getAll() -> [String] {
        items
    }
}

Deadlock Prevention Recommendations

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.

Best Practices for Working with Lock

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.

Release in finally

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.

Minimize Hold Time

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.

Consistent Lock Ordering

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

What is the difference between Lock and synchronized?

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.

What is a fair lock?

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.

How to avoid deadlock when using Lock?

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.

What is a Condition in Lock?

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.

Which Lock should I choose for a mobile application?

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

  • Lock is an explicit lock management interface from java.util.concurrent.locks.
  • ReentrantLock is the main implementation supporting reentrant acquisition and fairness.
  • ReadWriteLock separates read and write locks for read-heavy scenarios.
  • StampedLock adds optimistic reading for maximum performance.
  • Timeouts and Conditions are key advantages of Lock over synchronized.
  • Deadlock is prevented by consistent lock ordering and using tryLock.
  • In mobile development, coroutines (Android) and actors (iOS) are recommended.

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.

Discuss the project

Read also