Some code paths should not wait indefinitely for a lock.
That is especially true for:
- request threads with latency budgets
- duplicate maintenance work that should just skip
- tasks that can degrade gracefully instead of joining a long queue
This is where tryLock and timed lock acquisition become valuable.
Non-Blocking tryLock()
import java.util.concurrent.locks.ReentrantLock;
class TokenRefreshGuard {
private final ReentrantLock lock = new ReentrantLock();
boolean tryRefresh() {
if (!lock.tryLock()) {
return false;
}
try {
refresh();
return true;
} finally {
lock.unlock();
}
}
void refresh() {
}
}
This is useful when the right answer is:
- if another thread is already doing it, do not pile on
Timed tryLock
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
class TokenRefreshGuard {
private final ReentrantLock lock = new ReentrantLock();
boolean tryRefresh() throws InterruptedException {
if (!lock.tryLock(200, TimeUnit.MILLISECONDS)) {
return false;
}
try {
refresh();
return true;
} finally {
lock.unlock();
}
}
void refresh() {
}
}
This adds a waiting budget instead of an immediate yes/no decision.
Runnable Example
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockDemo {
public static void main(String[] args) throws Exception {
RefreshCoordinator coordinator = new RefreshCoordinator();
Thread first = new Thread(coordinator::runLongRefresh, "refresh-1");
first.start();
TimeUnit.MILLISECONDS.sleep(50);
boolean acquired = coordinator.tryServeAdminRefresh();
System.out.println("Second refresh acquired lock = " + acquired);
first.join();
}
static final class RefreshCoordinator {
private final ReentrantLock lock = new ReentrantLock();
void runLongRefresh() {
lock.lock();
try {
sleep(500);
System.out.println("Long refresh completed");
} finally {
lock.unlock();
}
}
boolean tryServeAdminRefresh() {
if (!lock.tryLock()) {
return false;
}
try {
System.out.println("Admin refresh executed");
return true;
} finally {
lock.unlock();
}
}
}
static void sleep(long millis) {
try {
TimeUnit.MILLISECONDS.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
}
Here the second operation chooses a policy of skipping rather than waiting behind the long-running holder.
That policy can be exactly right for some maintenance or duplicate-refresh paths.
Production-Style Scenarios
Good fits:
- cache refresh deduplication
- admin actions that should fail fast when the system is busy
- background consolidation tasks that are optional
- request paths with strict latency budgets
Poor fits:
- critical business updates that must eventually happen
- code with no clear fallback behavior after failure to acquire
The fallback policy is as important as the lock call itself.
Choosing Between Fail Fast and Bounded Waiting
tryLock() without waiting and timed tryLock(...) express two different product decisions.
Fail-fast acquisition says, “if the work is already in progress elsewhere, skip or return quickly.”
Timed acquisition says, “this work is still worth waiting for, but only up to a budget.”
That difference should match the user-visible behavior. Background refresh or duplicate maintenance work often fits fail-fast logic. Request handling with a small latency budget may fit timed waiting. Critical state changes with no acceptable fallback usually need a different design altogether.
Review Notes
A good review question is: what does the caller do when lock acquisition fails? If the answer is vague, the design is incomplete. Resilience does not come from the API alone; it comes from the fallback or timeout policy around it.
Key Takeaways
tryLock()is for fail-fast acquisition.- timed
tryLock()is for bounded waiting. - These tools are useful when indefinite blocking is not part of the design.
- A lock timeout without a clear fallback is incomplete design, not resilience.
Next post: Interruptible Lock Acquisition in Java
Comments