Before you split it, shrink it
A global lock is slow for one of two reasons, and only one of them is fixed by splitting.
If the lock is held across slow work, such as a payment call, a notification or a database write, then each booking holds it for milliseconds. Throughput is capped near one over that hold time however many cores you have. Moving the slow work outside the lock usually helps more than any split. The pattern is to decide and record the state change under the lock, release, then do the I/O. This is the hold-then-confirm shape from where the lock goes in a seat booking.
If the critical section is already short and threads still queue, the lock really is the bottleneck, and splitting is justified. Saying which case you are in, and how you would tell, is the first signal the interviewer wants. A profiler or thread dump showing many threads blocked on one monitor is the evidence.
Lock striping or a lock per resource
There are two ways to give each resource its own lock.
A map from resource id to lock, filled with computeIfAbsent, gives every room its own lock and no false contention. Its weakness is lifecycle. The map grows with every resource ever booked. Removing an unused entry is racy: one thread fetches the lock, another removes the entry, and a third creates a fresh lock for the same room. Two threads now hold "the" lock for one room at once.
Lock striping uses a fixed array of locks, and a resource maps to hash % stripes. Memory is bounded and nothing is ever removed, so the lifecycle race disappears. The price is that two unrelated rooms sometimes share a stripe and wait for each other. With many more stripes than threads competing at once, that shared waiting is rare, and the stripe count is the knob you tune. Java 7's ConcurrentHashMap made the same trade with its fixed set of segments.
Lock ordering by stripe, not by resource
A single-resource booking takes one stripe and cannot deadlock. A multi-resource booking, such as three meeting rooms for one conference or four seats in a group, needs several stripes at once. That is where a split lock can deadlock and a global lock never could.
The lock ordering rule from the deadlock page applies: one total order, used everywhere. With lock striping, the order must be over stripe indexes, not resource ids. Suppose room A maps to stripe 7 and room B to stripe 3, while room C maps to stripe 3 and room D to stripe 7. Booking {A, B} in resource-id order takes 7 then 3. Booking {C, D} in resource-id order takes 3 then 7. That is a cycle. Sorting by stripe index makes both take 3 then 7.
The second subtlety is duplicates. Two resources in one booking can share a stripe, so collapse the indexes before locking.
The caller locks, checks that every resource is free, marks them all booked, and unlocks in a finally. All-or-nothing comes from doing the whole check before the first write while every stripe is held. If you can use Guava, Striped.lock(n) with bulkGet does this job. It returns the stripes for a set of keys in ascending index order so that callers using them in that order cannot deadlock.
What tryLock adds, and what it does not
With a consistent order, deadlock is already impossible, so the timeout is not doing that job. It bounds how long a booking waits behind a slow holder, and it turns "stuck" into a result you can return, such as "busy, try again". The finally block matters as much as the timeout. A booking that got two of its three stripes and then timed out must release those two, or it has created the stall it was trying to avoid.
Without lock ordering, tryLock only changes the failure. Two bookings that each hold one stripe both time out, both release, and both retry in step. That is livelock, and only randomised backoff or ordering breaks it.
What the global lock was guarding by accident
This is where splits go wrong in production. A global lock serialises everything, so it quietly enforces invariants nobody wrote down. Before splitting, list them.
A per-user rule, such as "at most three active reservations", spans many resources and now has no lock. It needs a lock or atomic counter per user. That lock must have a fixed place in the order, for example user lock first, then resource stripes in ascending order, on every path.
A consistent view across resources, such as "every free room at 10:00", used to be one atomic read. Now it is either expensive, taking every stripe in order, or slightly stale. Usually you accept a snapshot for display and re-check under the right locks at booking time. Say so explicitly.
Shared bookkeeping, such as a booking-id sequence or an audit list, was free under one lock. It now needs an AtomicLong or a concurrent collection of its own.
The step people skip
The common mistake in this question is treating the split as a mechanical replacement: one lock becomes many, and the work looks done. The real work is the list of invariants the old lock enforced implicitly, each with a named new owner and a place in the acquisition order. A candidate who stripes the locks perfectly but loses the per-user quota has made the system faster and wrong. That is worse than slow and right.
Shrink the critical section before you split the lock. When you split it, order acquisitions by stripe index, release partial acquisitions on timeout, and give every invariant the global lock used to cover a new owner.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).