In a single process, a synchronized block is enough. Across many instances,
you need a lock that lives outside any one JVM. Redis, with an atomic set-if-
absent and an expiry, is a popular choice for this.
Acquire with SET NX PX
The key trick is atomicity: set the lock only if absent, with a TTL so a crashed holder can't lock forever. Store a unique token to prove ownership on release.
public boolean tryLock(String key, String token, Duration ttl) {
Boolean acquired = redis.opsForValue()
.setIfAbsent(key, token, ttl);
return Boolean.TRUE.equals(acquired);
}Release safely with a script
Never delete the key unconditionally — you might delete a lock another instance already acquired after yours expired. Use a Lua script that checks the token first, atomically.
private static final String UNLOCK =
"if redis.call('get', KEYS[1]) == ARGV[1] " +
"then return redis.call('del', KEYS[1]) else return 0 end";
public void unlock(String key, String token) {
redis.execute(RedisScript.of(UNLOCK, Long.class),
List.of(key), token);
}Use it around critical work
String token = UUID.randomUUID().toString();
if (lock.tryLock("job:nightly", token, Duration.ofMinutes(5))) {
try {
runNightlyJob();
} finally {
lock.unlock("job:nightly", token);
}
}Takeaways
SET key value NX PX ttlacquires atomically with automatic expiry.- Always release with a token check so you can't free someone else's lock.
- The TTL is a safety valve — size it larger than the worst-case work time.
For stronger guarantees under network partitions, look at the Redlock algorithm or a consensus system like ZooKeeper.