Industry Ready Java Spring Boot, React & Gen AI — Live Course
JavaMultithreading

Locks and Conditions

1. Introduction

In earlier topics, we learned how Java provides synchronization using the synchronized keyword. While synchronized works well for many situations, it has some limitations when building complex concurrent systems.

To provide more flexibility and control over locking behavior, Java introduced the Lock framework in the package:

java.util.concurrent.locks

The most commonly used class in this framework is:

ReentrantLock

Locks allow developers to:

  • explicitly acquire and release locks
  • attempt locks with timeout
  • interrupt waiting threads
  • create multiple waiting conditions

Along with locks, Java also provides Condition objects, which allow threads to coordinate in a way similar to wait() and notify() but with more flexibility.

2. Why Locks Are Needed

The synchronized keyword automatically manages locks. However, it has some limitations:

  • no ability to try acquiring a lock without blocking
  • no timeout when waiting for a lock
  • limited control over waiting conditions
  • no multiple condition queues

The Lock API provides additional features such as:

  • tryLock()
  • lockInterruptibly()
  • fairness policies
  • multiple condition variables

These features make locks useful in highly concurrent and complex systems.

3. The Lock Interface

The core interface of the lock framework is:

Lock

It defines methods for acquiring and releasing locks.

Important methods include:

MethodDescription
lock()Acquires the lock
unlock()Releases the lock
tryLock()Attempts to acquire lock without waiting
lockInterruptibly()Allows thread interruption while waiting
newCondition()Creates a condition variable

The most common implementation is:

ReentrantLock

4. What is ReentrantLock

ReentrantLock works similarly to synchronized, but provides additional flexibility.

Example:

import java.util.concurrent.locks.ReentrantLock;

class Counter {

    private int count = 0;
    private ReentrantLock lock = new ReentrantLock();

    void increment() {

        lock.lock();

        try {
            count++;
        } finally {
            lock.unlock();
        }

    }

}

Important rule:

Whenever lock() is used, it must be followed by unlock() in a finally block.

This ensures the lock is released even if an exception occurs.

5. Reentrancy in ReentrantLock

The word reentrant means that the same thread can acquire the same lock multiple times.

Example:

class Example {

    ReentrantLock lock = new ReentrantLock();

    void methodA() {

        lock.lock();

        try {
            methodB();
        } finally {
            lock.unlock();
        }

    }

    void methodB() {

        lock.lock();

        try {
            System.out.println("Reentrant lock example");
        } finally {
            lock.unlock();
        }

    }

}

If a thread already holds the lock, it can acquire it again without causing a deadlock.

The lock keeps track of how many times it has been acquired.

6. tryLock()

One powerful feature of the Lock API is tryLock().

This method attempts to acquire a lock without blocking the thread indefinitely.

Example:

ReentrantLock lock = new ReentrantLock();

if(lock.tryLock()) {

    try {
        System.out.println("Lock acquired");
    } finally {
        lock.unlock();
    }

} else {
    System.out.println("Could not acquire lock");
}

This is useful in situations where threads should not wait forever.

7. tryLock with Timeout

Locks can also attempt acquisition with a timeout.

Example:

if(lock.tryLock(2, TimeUnit.SECONDS)) {

    try {
        System.out.println("Lock acquired");
    } finally {
        lock.unlock();
    }

} else {
    System.out.println("Timeout waiting for lock");
}

This prevents threads from waiting indefinitely for a lock.

8. Fair Locks

ReentrantLock can optionally be created as a fair lock.

Example:

ReentrantLock lock = new ReentrantLock(true);

Fair lock behavior:

  • threads acquire the lock in the order they requested it
  • prevents thread starvation

However, fair locks may reduce performance because the scheduler cannot optimize execution as aggressively.

9. Condition Objects

A Condition object works similarly to wait() and notify(), but is used with Lock.

It allows threads to:

  • wait for a condition
  • signal other waiting threads

A Condition object is created using:

Condition condition = lock.newCondition();

Key methods:

MethodDescription
await()Thread waits
signal()Wakes one waiting thread
signalAll()Wakes all waiting threads

Conditions provide more control than traditional wait() and notify().

10. Example Using Condition

import java.util.concurrent.locks.*;

class SharedResource {

    private int data;
    private boolean available = false;

    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    void produce(int value) throws InterruptedException {

        lock.lock();

        try {

            while(available) {
                condition.await();
            }

            data = value;
            available = true;

            System.out.println("Produced: " + value);

            condition.signal();

        } finally {
            lock.unlock();
        }

    }

    void consume() throws InterruptedException {

        lock.lock();

        try {

            while(!available) {
                condition.await();
            }

            System.out.println("Consumed: " + data);

            available = false;

            condition.signal();

        } finally {
            lock.unlock();
        }

    }

}

This example behaves similarly to the wait/notify producer-consumer model but uses the Lock API.

11. Locks vs Synchronized

Both locks and synchronized provide mutual exclusion, but they differ in flexibility.

FeaturesynchronizedReentrantLock
Lock acquisitionAutomaticManual
Timeout supportNoYes
Interruptible lockNoYes
Multiple condition variablesNoYes
Ease of useSimplerMore flexible

Use synchronized for simple cases and ReentrantLock for advanced concurrency scenarios.

12. When to Use Locks

Locks are useful when:

  • advanced thread coordination is needed
  • timeout-based locking is required
  • interruptible locks are necessary
  • multiple waiting conditions must be supported
  • building high-performance concurrent systems

Examples include:

  • thread pools
  • resource pools
  • concurrent data structures
  • producer-consumer pipelines

13. Best Practices

When using locks:

  • always release locks in finally
  • avoid long critical sections
  • prefer higher-level concurrency utilities when possible
  • use fair locks only when necessary
  • document locking strategy clearly

Improper use of locks can lead to deadlocks and performance problems.

14. Summary

The Lock framework provides a more flexible alternative to the synchronized keyword for controlling access to shared resources.

ReentrantLock allows explicit lock management and supports advanced features such as:

  • timed lock acquisition
  • interruptible locking
  • fairness policies

Condition objects allow threads to wait and signal events similar to wait() and notify() but with greater flexibility.

These tools are essential when building advanced concurrent systems where precise control over locking and thread coordination is required.

Written By: Shiva Srivastava

How is this guide?

Last updated on