Improved locking documentation

This commit is contained in:
leventov
2015-05-29 03:26:29 +01:00
parent 177f3996c1
commit 02fd928d25
4 changed files with 261 additions and 13 deletions
@@ -10,7 +10,14 @@ package net.openhft.chronicle.hash;
public interface ExternalHashQueryContext<K> extends HashQueryContext<K>, AutoCloseable {
/**
* Could throw only unchecked exception.
* Closes the query context, automatically releases all locks and disposes all resources,
* acquired during the query operation. I. e. you shouldn't release locks manually in the end
* of try-with-resources statement: <pre>{@code
* try (ExternalHashQueryContext<K> q = hash.queryContext(key))
* q.writeLock().lock();
* // ...make a query under exclusive lock
* // NOT NEEDED - q.readLock().unlock();
* }}</pre>
*/
@Override
void close();
@@ -1,9 +1,92 @@
package net.openhft.chronicle.hash;
import net.openhft.chronicle.hash.locks.IllegalInterProcessLockStateException;
import net.openhft.chronicle.hash.locks.InterProcessLock;
import net.openhft.chronicle.hash.locks.InterProcessReadWriteUpdateLock;
import net.openhft.chronicle.map.MapMethods;
import net.openhft.chronicle.map.MapQueryContext;
import net.openhft.chronicle.map.ReturnValue;
/**
* Context of {@link ChronicleHash} operations with <i>individual keys</i>.
*
* <p>This context provides access to {@link InterProcessReadWriteUpdateLock}, governing access
* to the entry. Your have no chance to perform any actions under race with other threads, because
* all required locks are acquired automatically on each operation, you should deal with locks
* manually in the following cases:
* <ul>
* <li>You should perform some read operation, and then write. On first operation, the context
* will automatically acquire the read lock; on the second, it will try to acquire the update
* or write lock, but it will fail with {@link IllegalMonitorStateException}, because this is
* dead lock prone. (See {@link InterProcessReadWriteUpdateLock} documentation for explanation.
* So, such context code is incorrect: <pre>{@code
* // INCORRECT
* try (ExternalMapQueryContext<K, V, ?> q = map.queryContext(key)) {
* // q.entry(), checks if the entry is present in the map, and acquires
* // the read lock for that.
* if (q.entry() != null) {
* // Tries to acquire the write lock to perform modification,
* // but this is an illegal upgrade: read -> write, throws IllegalMonitorStateException
* q.remove(q.entry());
* }
* }}</pre>
* So, to workaround this, you should acquire the {@linkplain #updateLock() update lock},
* which is upgradable to the write lock, <i>before</i> performing any reading in the context:
* <pre>{@code
* // CORRECT
* try (ExternalMapQueryContext<K, V, ?> q = map.queryContext(key)) {
* q.updateLock().lock(); // acquire the update lock before checking the entry presence.
* if (q.entry() != null) {
* q.remove(q.entry());
* }
* }}</pre>
* </li>
* <li>You want to try to acquire some lock heuristically, in order to improve total {@code
* ChronicleHash} concurrency. You should base on probabilities of making some reading or
* writing operations. For example, see how {@link MapMethods#acquireUsing} might be
* implemented:<pre>{@code
* default void acquireUsing(MapQueryContext<K, V, R> q, ReturnValue<V> returnValue) {
* // For acquireUsing(), it is assumed to be very probable, that the entry is already
* // present in the map, so we will perform the whole acquireUsing() without exclusive locking
* if (q.readLock().tryLock()) {
* MapEntry<?, V> entry = q.entry();
* if (entry != null) {
* // Entry is present, return
* returnValue.returnValue(entry.value());
* return;
* }
* // Key is absent
* // Need to unlock, to lock to update lock later. Direct updrage is forbidden.
* q.readLock().unlock();
* }
* // We are here, either if we:
* // 1) Failed to acquire the read lock, this means some other thread is holding the write
* // lock now, in this case waiting for the update lock acquisition is no longer, than for
* // the read
* // 2) Seen the entry is absent under the read lock. This means we need to insert
* // the default value into the map. that requires update-level access as well
* q.updateLock().lock();
* MapEntry<?, V> entry = q.entry();
* if (entry != null) {
* // Entry is present, return
* returnValue.returnValue(entry.value());
* return;
* }
* // Key is absent
* q.insert(q.absentEntry(), q.defaultValue(q.absentEntry()));
* returnValue.returnValue(q.entry().value());
* }}</pre></li>
* <li>If the default {@link InterProcessLock#lock()} policy of trying to acquire the
* lock for some time, and then throw {@code RuntimeException}, or need custom timeout:
* <pre>{@code
* try (ExternalHashQueryContext<K> q = hash.queryContext(key)) {
* if (q.writeLock().tryLock(5, TimeUnit.SECONDS)) {
* // do something
* } else {
* // do something else, maybe not throwing an exception
* }
* }}</pre></li>
* </ul>
*
* @param <K> the hash key type
* @see ChronicleHash#queryContext(Object)
@@ -16,30 +16,111 @@
package net.openhft.chronicle.hash.locks;
import net.openhft.chronicle.hash.ChronicleHash;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* - Inter-process,
* - unfair,
* - busy-waiting,
* - reentrant,
* - single-thread (paradox? :) Once a lock object obtained, it shouldn't be stored in a field and
* accessed from multiple threads, instead of that, lock objects should be obtained in each thread
* separately, using the same call chain. This is because since the lock is inter-process, it
* anyway keep the syncronization state in shared memory, but restricting on-heap "view" of
* shared lock is beneficial form perf POV, e. g., fields of the on-heap lock object shouldn't
* be volatile.
* - doesn't support conditions.
* Abstracts inter-process lock of {@link ChronicleHash} data: segments, entries.
*
* <p>This lock is not reentrant, but kind of "saturating": multiple {@link #lock()} calls has
* the same effect as a single call. Likewise {@link #unlock()} -- multiple unlocks, or "unlocking"
* when the lock isn't actually held, has no negative effects.
*
* <p>Once a lock object obtained, it <i>shouldn't be stored in a field and accessed from multiple
* threads</i>, instead of that, lock objects should be obtained in each thread separately, using
* the same call chain. This is because since the lock is inter-process, it anyway keeps the
* synchronization state in shared memory, but restricting on-heap "view" of shared lock to a single
* thread is beneficial form performance point-of-view, e. g. fields of the on-heap
* {@code InterProcessLock} object shouldn't be {@code volatile}.
*
* <p>Locks is inter-process, hence it cannot afford to wait for acquisition infinitely, because
* it would be too dead-lock prone. {@link #lock()} throws {@code RuntimeException} after some
* finite time.
*
* @implNote Inter-process lock is unfair.
*
* @see InterProcessReadWriteUpdateLock
*/
public interface InterProcessLock extends Lock {
/**
* Checks if this lock is held by current thread.
*/
boolean isHeldByCurrentThread();
/**
* Conditions are not supported by inter-process locks.
* Acquires the lock, if this lock (or stronger-level lock, in the context of {@link
* InterProcessReadWriteUpdateLock}) is already held by current thread, this call returns
* immediately.
*
* <p>If the lock is not available then the current thread enter a busy loop. After some
* specified threshold, the thread <i>might</i> be disabled for thread scheduling purposes
* and lies dormant until the lock has been acquired. After another specified threshold
* {@link RuntimeException} is thrown.
*
* @throws IllegalInterProcessLockStateException if this method call observes illegal lock
* state, or some lock limitations reached (e. g. maximum read lock holders)
* @throws RuntimeException if fails to acquire a lock for some finite time
*/
@Override
void lock();
/**
* Acquires the lock only if it is free at the time of invocation, also if the lock is already
* held by the current thread, this call immediately returns {@code true}.
*
* <p>Acquires the lock if it is available and returns immediately
* with the value {@code true}.
* If the lock is not available then this method will return
* immediately with the value {@code false}.
*
* <p>Example usage: <pre>{@code
* try (ExternalMapQueryContext<K, V, ?> q = map.queryContext(key)) {
* if (q.updateLock().tryLock()) {
* // highly-probable branch
* if (q.entry() != null) {
* // upgrade to write lock
* q.writeLock().lock();
* q.remove(q.entry());
* } else {
* // ...
* }
* } else {
* // if failed to acquire the update lock without waiting, go acquire the write lock
* // right away, because probability that we will need to upgrade to write lock anyway
* // is high.
* q.writeLock().lock();
* if (q.entry() != null) {
* q.remove(q.entry());
* } else {
* // ...
* }
* }
* }}</pre>
*
* @return {@code true} if the lock was acquired and {@code false} otherwise
* @throws IllegalInterProcessLockStateException if this method call observes illegal lock
* state, or some lock limitations reached (e. g. maximum read lock holders)
*/
@Override
boolean tryLock();
/**
* Releases the lock, if the lock is not held by the current thread, returns immediately.
*
* @throws IllegalInterProcessLockStateException if this method call observes illegal lock
* state
*/
@Override
void unlock();
/**
* Conditions are not supported by inter-process locks, always throws {@code
* UnsupportedOperationException}.
*
* @return nothing, always throws an exception
* @throws UnsupportedOperationException always
@@ -16,20 +16,97 @@
package net.openhft.chronicle.hash.locks;
import net.openhft.chronicle.hash.ExternalHashQueryContext;
import net.openhft.chronicle.hash.HashQueryContext;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
/**
* Tri-level lock, for efficient handling of concurrent accesses, that requre different privileges.
* For grasping multi-level lock concept and basic semantic principles, please read
* {@link ReadWriteLock} documentation.
*
* <p>{@linkplain #readLock() Read lock} is for "readers". Multiple threads might hold read lock
* simultaneously. Read lock held by any thread prevents write lock from being acquired.
*
* <p>{@linkplain #updateLock() Update lock} is for writers, who must access the resource
* exclusively with each other, but write safely for concurrent readers. Only one thread could hold
* update or write lock at the same time, but update lock doesn't prevent concurrent readers
* to come.
*
* <p>{@linkplain #writeLock() Write lock} is for writers, which need total exclusiveness. When
* write lock is held, nobody may access the resource at the same time.
*
* <p>Same as {@link InterProcessLock} itself, the {@code InterProcessReadWriteUpdateLock} as a
* whole is <i>saturating</i>: when stronger-level lock is acquired, all weaker-level locks
* are thought to be acquired as well ("saturated"). Unlocking a weaker-level lock "desaturates"
* it with stronger-level locks only. This means that try-finally pattern looks unusual: <pre>{@code
* l.writeLock().lock(); // acquires all three level locks
* try {
* // do something
* } finally {
* l.readLock().unlock();
* // l.writeLock().unlock(); - INCORRECT, because "desaturates" only write-level,
* // leaving read lock and update lock held
* }}</pre>
* However, if you use {@code InterProcessReadWriteUpdateLock} as a part of {@link
* ExternalHashQueryContext} query, you shouldn't keep this peculiarity in mind, because {@link
* ExternalHashQueryContext#close()} handles unlocking for you.
*
* <p>Any downgrades ("desaturations") are possible, but only update -> write upgrade is allowed.
* This is so to prevent dead locks: for example, imagine, that two threads came and acquired read
* locks, and then both try to upgrade to write lock, blocking each other indefinitely.
*
* <p>This interface is based on <a href="https://code.google.com/p/concurrent-locks/">this
* work</a>, might be useful to read to understand read-write-update lock idea, and how to use
* this concept.
*
* <p>Same as {@code InterProcessLock}, instances of this interface are for single-thread use
* only. Views of the same inter-process lock should be obtained in different threads independently.
* See {@link InterProcessLock} documentation for rationalization of this.
*
* @see InterProcessLock
* @see ReadWriteLock
*/
public interface InterProcessReadWriteUpdateLock extends ReadWriteLock {
/**
* Returns the read-level lock. Calling {@link InterProcessLock#unlock() unlock()} on this
* lock also releases the update and write locks, if held, as well.
*
* @return the read level lock
*/
@NotNull
@Override
InterProcessLock readLock();
/**
* Returns the update-level lock. Calling {@link InterProcessLock#lock() lock()} or other
* locking methods ({@code tryLock()}, etc.) on this lock acquires the read lock, as well.
* Calling {@link InterProcessLock#unlock() unlock()} on this lock also releases the write lock.
*
* <p>Any attempt to acquire this lock (including {@code tryLock()}, if the read lock is already
* held by the current thread, but the update lock is not yet, is prevented by throwing {@link
* IllegalInterProcessLockStateException}.
*
* @return the update-level lock
*/
@NotNull
InterProcessLock updateLock();
/**
* Returns the write-level lock. Calling {@link InterProcessLock#lock() lock()} or other
* locking methods ({@code tryLock()}, etc.) on this lock acquires the read and update locks,
* as well.
*
* <p>Any attempt to acquire this lock (including {@code tryLock()}, if the read lock is already
* held by the current thread, but the update and write locks are not yet, is prevented by
* throwing {@link IllegalInterProcessLockStateException}.
*
* @return the update-level lock
*/
@NotNull
@Override
InterProcessLock writeLock();