Class NeoLock

java.lang.Object
org.ores.async.NeoLock

public class NeoLock extends Object
An async (non-blocking) mutex.

Unlike synchronized blocks and ReentrantLock, NeoLock does not park a thread while waiting. Waiters enqueue an error-first callback; when the lock becomes free, the next waiter's callback fires with a fresh Unlock token. This makes it suitable for callback chains where the acquire and the release happen on different threads (or different virtual threads), which is the common case with async I/O.

Why not just use synchronized?

Two reasons:

  1. Cross-thread release. A synchronized block must be released by the same thread that acquired it. Inside a callback chain, the release usually happens inside an async completion handler running on a different worker. NeoLock models this directly: acquire returns a token via callback, release happens via that token.
  2. VT-friendly under all JDKs. On JDK 21 with VTs, synchronized used to pin the carrier thread; JEP 491 (JDK 24) removes that constraint, but NeoLock never had it.

The continuation that acquire(org.ores.async.Asyncc.IAsyncCallback<org.ores.async.Unlock, java.lang.Object>) fires is conventionally named c in code examples (short for continuation). It receives the Unlock token instead of a value.

Usage

   NeoLock lock = new NeoLock("inventory");

   lock.acquire((err, unlock) -> {
       try {
           // critical section. Safe to await async work inside here; the lock is held until
           // unlock.releaseLock() is called.
           mutateInventory(itemId);
       } finally {
           unlock.releaseLock();
       }
   });
 

For a critical section that awaits async work:

   lock.acquire((err, unlock) -> {
       fetchCurrentBalance(accountId, (e, balance) -> {
           updateBalance(accountId, balance.subtract(amount), (e2, ok) -> {
               unlock.releaseLock(); // released after the async update completes
               reply.send(ok);
           });
       });
   });
 

Calling unlock.releaseLock() a second time is a programming bug and logs a stack trace to System.err but does not throw past the call site (which is usually a finally block where re-throwing would mask the real error).

Concurrency contract (v0.2.x)

  • The waiter queue and the locked flag are both guarded by the NeoLock instance's monitor. (Earlier versions split those across two locks, allowing two concurrent acquirers in a tight race — fixed in v0.2.0.)
  • Waiter queue is an ArrayDeque, popped from the head, pushed to the tail — FIFO ordering of pending acquirers.
  • Unlock is a public sibling class (in v0.2.2+) so NeoLock is usable from any package.
See Also: