Class NeoLock
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?
- Cross-thread release. A
synchronizedblock 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.NeoLockmodels this directly: acquire returns a token via callback, release happens via that token. - VT-friendly under all JDKs. On JDK 21 with VTs,
synchronizedused to pin the carrier thread; JEP 491 (JDK 24) removes that constraint, butNeoLocknever had it.
The continuation that acquire(long, 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 synchronous critical sections, use withLock(Runnable) which auto-releases on
normal return and on exception, eliminating the finally{} boilerplate:
lock.withLock(() -> mutateInventory(itemId)); // even if mutateInventory throws, the lock is released.
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);
});
});
});
Non-reentrance
NeoLock is not reentrant. Calling acquire(long, org.ores.async.Asyncc.IAsyncCallback<org.ores.async.Unlock, java.lang.Object>) from inside an
acquire callback that has not yet been released will enqueue the new request behind the
current holder — if the current holder is the same logical "owner" waiting on its own
release, the callback chain stalls indefinitely. There is no general way to detect this in
an async/callback world (release may be on a different thread or scheduled later); if you
need a defensive check, use tryAcquire() which returns immediately when the lock is
held.
VT-pinning audit (JDK 21 - 23)
Internally NeoLock uses synchronized (this) blocks to guard the
locked flag and the waiter queue. On JDK 21 - 23, synchronized pins the
carrier thread while the monitor is held; on JDK 24+ ([JEP 491]) it does not. The critical
sections are microseconds (one flag check, one deque push or pop) so the pinning impact is
negligible in practice. Audited at v0.2.5 with a 1 000-acquirer stress test
(NeoLockFairnessTest#stress_1000_acquirers_no_lost_wakeups) on JDK 17; no observable
latency floor or thread-starvation. A future major release may migrate the internal monitor
to a ReentrantLock for explicit VT-friendliness on older
JDKs.
Concurrency contract
- FIFO fairness: acquirers receive the lock in the order they called
acquire(...). Pinned byNeoLockFairnessTest(100 sequential acquirers in order). Internally backed by anArrayDequepopped from head, pushed to tail. - The waiter queue and the
lockedflag are both guarded by theNeoLockinstance's monitor. (Earlier versions split those across two locks, allowing two concurrent acquirers in a tight race — fixed in v0.2.0.) Unlockis a public sibling class (since v0.2.2) soNeoLockis usable from any package.- v0.2.5 adds
tryAcquire(),acquire(long, Asyncc.IAsyncCallback)with timeout,withLock(Runnable),isLocked(),queueDepth().
-
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionvoidacquire(long timeoutMs, Asyncc.IAsyncCallback<Unlock, Object> cb) Acquire the lock with a bounded wait.voidbooleanisLocked()Snapshot of whether the lock is currently held.makeUnlock(boolean isImmediate) intSnapshot of the number of waiters currently queued behind the holder.Non-blocking attempt to acquire the lock.voidAcquire the lock, run thecriticalSectionsynchronously, release.
-
Constructor Details
-
NeoLock
public NeoLock() -
NeoLock
-
-
Method Details
-
getNamespace
-
isLocked
public boolean isLocked()Snapshot of whether the lock is currently held. Useful for diagnostics / metrics; not a substitute fortryAcquire()in a check-then-act sequence (the lock state may change between theisLocked()read and a subsequentacquire(...)call).- Since:
- 0.2.5
-
queueDepth
public int queueDepth()Snapshot of the number of waiters currently queued behind the holder. Useful for diagnostics and metrics. The count excludes the holder itself.- Since:
- 0.2.5
-
tryAcquire
Non-blocking attempt to acquire the lock. Returns anOptionalcontaining anUnlocktoken if the lock was free and is now held by this caller, or an emptyOptionalif the lock was already held.Callers must
Unlock.releaseLock()the token when done. The lock is held until release; subsequenttryAcquire()calls (and queuedacquire(...)calls) will be denied/wait respectively until then.- Since:
- 0.2.5
-
acquire
Acquire the lock with a bounded wait. If the lock is not acquired withintimeoutMsmilliseconds, fires the callback with aTimeoutExceptionas the error. A pending acquire is cancelled cleanly (removed from the waiter queue) on timeout.If
timeoutMs <= 0, this is equivalent totryAcquire(): succeeds immediately if available, fails immediately withTimeoutExceptionotherwise.If the lock is granted just as the timeout fires (a race), the lock is released on the caller's behalf and the callback fires with the
TimeoutExceptiononly once.- Since:
- 0.2.5
-
withLock
Acquire the lock, run thecriticalSectionsynchronously, release. The lock is released even if the critical section throws — eliminating the standardtry / finally / unlock.releaseLock()boilerplate.The critical section is synchronous: it runs to completion on the acquire-callback thread, and the lock is released as soon as it returns or throws. If you need to await async work inside a critical section, use
acquire(Asyncc.IAsyncCallback)directly so you can decide when to callUnlock.releaseLock().Any exception thrown by
criticalSectionpropagates to the caller of the acquire-callback thread after the lock is released. The lock will never leak due to a sync throw.- Since:
- 0.2.5
-
makeUnlock
-
acquire
-