From 02fd928d25ea89082cf8b0f90a6243aafc3a349e Mon Sep 17 00:00:00 2001 From: leventov Date: Fri, 29 May 2015 03:25:46 +0100 Subject: [PATCH] Improved locking documentation --- .../hash/ExternalHashQueryContext.java | 9 +- .../chronicle/hash/HashQueryContext.java | 83 ++++++++++++++ .../hash/locks/InterProcessLock.java | 105 ++++++++++++++++-- .../InterProcessReadWriteUpdateLock.java | 77 +++++++++++++ 4 files changed, 261 insertions(+), 13 deletions(-) diff --git a/src/main/java/net/openhft/chronicle/hash/ExternalHashQueryContext.java b/src/main/java/net/openhft/chronicle/hash/ExternalHashQueryContext.java index 6e4639223..f59a4f22a 100644 --- a/src/main/java/net/openhft/chronicle/hash/ExternalHashQueryContext.java +++ b/src/main/java/net/openhft/chronicle/hash/ExternalHashQueryContext.java @@ -10,7 +10,14 @@ package net.openhft.chronicle.hash; public interface ExternalHashQueryContext extends HashQueryContext, 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:
{@code
+     * try (ExternalHashQueryContext q = hash.queryContext(key))
+     *     q.writeLock().lock();
+     *     // ...make a query under exclusive lock
+     *     // NOT NEEDED - q.readLock().unlock();
+     * }}
*/ @Override void close(); diff --git a/src/main/java/net/openhft/chronicle/hash/HashQueryContext.java b/src/main/java/net/openhft/chronicle/hash/HashQueryContext.java index d3b2269ad..d0529f993 100644 --- a/src/main/java/net/openhft/chronicle/hash/HashQueryContext.java +++ b/src/main/java/net/openhft/chronicle/hash/HashQueryContext.java @@ -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 individual keys. + * + *

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: + *

    + *
  • 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:
    {@code
    + * // INCORRECT
    + * try (ExternalMapQueryContext 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());
    + *     }
    + * }}
    + * So, to workaround this, you should acquire the {@linkplain #updateLock() update lock}, + * which is upgradable to the write lock, before performing any reading in the context: + *
    {@code
    + * // CORRECT
    + * try (ExternalMapQueryContext q = map.queryContext(key)) {
    + *     q.updateLock().lock(); // acquire the update lock before checking the entry presence.
    + *     if (q.entry() != null) {
    + *         q.remove(q.entry());
    + *     }
    + * }}
    + *
  • + *
  • 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:
    {@code
    + * default void acquireUsing(MapQueryContext q, ReturnValue 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 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 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());
    + * }}
  • + *
  • 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: + *
    {@code
    + * try (ExternalHashQueryContext q = hash.queryContext(key)) {
    + *     if (q.writeLock().tryLock(5, TimeUnit.SECONDS)) {
    + *         // do something
    + *     } else {
    + *         // do something else, maybe not throwing an exception
    + *     }
    + * }}
  • + *
* * @param the hash key type * @see ChronicleHash#queryContext(Object) diff --git a/src/main/java/net/openhft/chronicle/hash/locks/InterProcessLock.java b/src/main/java/net/openhft/chronicle/hash/locks/InterProcessLock.java index 459d8fce8..c924eca78 100644 --- a/src/main/java/net/openhft/chronicle/hash/locks/InterProcessLock.java +++ b/src/main/java/net/openhft/chronicle/hash/locks/InterProcessLock.java @@ -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. + * + *

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. + * + *

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 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}. + * + *

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. + * + *

If the lock is not available then the current thread enter a busy loop. After some + * specified threshold, the thread might 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}. + * + *

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}. + * + *

Example usage:

{@code
+     * try (ExternalMapQueryContext 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 {
+     *             // ...
+     *         }
+     *     }
+     * }}
+ * + * @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 diff --git a/src/main/java/net/openhft/chronicle/hash/locks/InterProcessReadWriteUpdateLock.java b/src/main/java/net/openhft/chronicle/hash/locks/InterProcessReadWriteUpdateLock.java index a20cd964e..6cdfab6a5 100644 --- a/src/main/java/net/openhft/chronicle/hash/locks/InterProcessReadWriteUpdateLock.java +++ b/src/main/java/net/openhft/chronicle/hash/locks/InterProcessReadWriteUpdateLock.java @@ -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. + * + *

{@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. + * + *

{@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. + * + *

{@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. + * + *

Same as {@link InterProcessLock} itself, the {@code InterProcessReadWriteUpdateLock} as a + * whole is saturating: 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:

{@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
+ * }}
+ * 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. + * + *

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. + * + *

This interface is based on this + * work, might be useful to read to understand read-write-update lock idea, and how to use + * this concept. + * + *

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. + * + *

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. + * + *

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();