mirror of
https://github.com/OpenHFT/Chronicle-Map.git
synced 2021-09-13 01:26:12 +00:00
Improved locking documentation
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user