Resource Management
Concurrency Control
Conditional Locking
Software Development
Programming Techniques

Conditionally lock resources

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Conditionally locking a resource means you do not blindly acquire a lock every time. Instead, you lock only when the state or operation requires exclusive access. Done well, this reduces contention. Done badly, it creates races because the condition is checked outside the protection of the lock.

The Key Rule: Check Shared Conditions Under the Lock

The most important rule is that conditions involving shared mutable state should usually be checked while holding the relevant lock.

Unsafe pattern:

java
1if (!resourceInUse) {
2    synchronized (lock) {
3        resourceInUse = true;
4    }
5}

Two threads can observe resourceInUse == false before either enters the synchronized block.

Safer pattern:

java
1synchronized (lock) {
2    if (!resourceInUse) {
3        resourceInUse = true;
4    }
5}

The condition and the update must be part of the same critical section if they protect the same shared fact.

Use tryLock() for Opportunistic Access

Sometimes the right meaning of "conditional locking" is "lock only if the resource is immediately available." In Java, ReentrantLock.tryLock() expresses that directly.

java
1import java.util.concurrent.locks.ReentrantLock;
2
3ReentrantLock lock = new ReentrantLock();
4
5if (lock.tryLock()) {
6    try {
7        System.out.println("Acquired lock and using resource");
8    } finally {
9        lock.unlock();
10    }
11} else {
12    System.out.println("Resource busy, skipping work");
13}

This is useful when waiting is worse than skipping or retrying later.

Conditional Waiting Is Different from Conditional Acquisition

Another common case is when a thread must wait until some condition becomes true before using the resource. That is better modeled with a condition variable rather than repeated polling.

java
1import java.util.concurrent.locks.Condition;
2import java.util.concurrent.locks.ReentrantLock;
3
4class BoundedBuffer {
5    private final ReentrantLock lock = new ReentrantLock();
6    private final Condition notEmpty = lock.newCondition();
7    private String value;
8
9    public void put(String newValue) {
10        lock.lock();
11        try {
12            value = newValue;
13            notEmpty.signalAll();
14        } finally {
15            lock.unlock();
16        }
17    }
18
19    public String take() throws InterruptedException {
20        lock.lock();
21        try {
22            while (value == null) {
23                notEmpty.await();
24            }
25            String result = value;
26            value = null;
27            return result;
28        } finally {
29            lock.unlock();
30        }
31    }
32}

Here the lock is always used, but access to the resource is still conditional on the shared state.

Keep Lock Scope Small

Conditionally locking resources is often motivated by performance. A big improvement usually comes from reducing how long the lock is held.

Bad pattern:

  • lock resource
  • do expensive unrelated work
  • unlock resource

Better pattern:

  • prepare what you can outside the lock
  • lock only for shared-state access
  • unlock immediately after the shared update

This does more for throughput than many clever locking tricks.

Lock Ordering Still Matters

If several resources may be locked conditionally, deadlock risk still exists. For example, thread A might lock resource 1 then wait for resource 2, while thread B does the opposite.

A simple preventive rule is to always acquire multiple locks in a fixed order.

Even sophisticated conditional logic does not rescue a design that ignores lock ordering.

Sometimes an Atomic Variable Is Better

If the condition is only a simple state transition, a full lock may be unnecessary. An atomic compare-and-set can model conditional acquisition more directly.

java
1import java.util.concurrent.atomic.AtomicBoolean;
2
3AtomicBoolean inUse = new AtomicBoolean(false);
4
5if (inUse.compareAndSet(false, true)) {
6    try {
7        System.out.println("Acquired logical ownership");
8    } finally {
9        inUse.set(false);
10    }
11}

This is not a universal replacement for locks, but for small ownership flags it is often cleaner.

Choose the Semantics You Actually Need

"Conditionally lock" can mean several things:

  • lock only if free now
  • wait until state becomes valid
  • lock only around the shared update
  • attempt ownership using an atomic state flag

These are different concurrency problems. Choosing the wrong primitive is often why locking code becomes confusing.

Common Pitfalls

  • Checking a shared condition outside the lock and then acting on stale state.
  • Using polling loops when a condition variable would express the wait more clearly.
  • Holding a lock longer than necessary and turning a small critical section into a bottleneck.
  • Using tryLock() without a clear policy for what should happen on failure.
  • Forgetting lock-ordering rules when conditionally acquiring multiple resources.

Summary

  • Conditional locking is about acquiring or using a resource only when the state requires it.
  • Shared-state conditions usually must be checked under the same lock that protects the state.
  • 'tryLock() is useful for opportunistic access, while condition variables are better for waiting until a condition becomes true.'
  • Keep lock scope small and lock ordering consistent.
  • Sometimes an atomic state transition is a better fit than a full lock.

Course illustration
Course illustration

All Rights Reserved.