This is an automated email from the ASF dual-hosted git repository. chtompki pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-lang.git
commit bf7aa89ca45706ad1f40cacdf7448626529dd6b0 Author: Jochen Wiedmann <jochen.wiedm...@gmail.com> AuthorDate: Fri Jun 26 11:51:06 2020 +0200 Adding Javadocs for the Locks.Lock class. Improvements in the test suite. --- .../org/apache/commons/lang3/concurrent/Locks.java | 178 ++++++++++++++++++++- .../apache/commons/lang3/concurrent/LocksTest.java | 37 +++++ 2 files changed, 213 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/commons/lang3/concurrent/Locks.java b/src/main/java/org/apache/commons/lang3/concurrent/Locks.java index 28df9c0..48e16eb 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/Locks.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/Locks.java @@ -70,31 +70,174 @@ import org.apache.commons.lang3.function.FailableFunction; */ public class Locks { + /** + * This class implements a wrapper for a locked (hidden) object, and + * provides the means to access it. The basic idea, is that the user + * code forsakes all references to the locked object, using only the + * wrapper object, and the accessor methods + * {@link #acceptReadLocked(FailableConsumer)}, + * {@link #acceptWriteLocked(FailableConsumer)}, + * {@link #applyReadLocked(FailableFunction)}, and + * {@link #applyWriteLocked(FailableFunction)}. By doing so, the + * necessary protections are guaranteed. + * @param <O> The locked (hidden) objects type. + * + * @since 3.11 + */ public static class Lock<O extends Object> { - private final StampedLock lock = new StampedLock(); private final O lockedObject; + /** + * Creates a new instance with the given locked object. This + * constructor is supposed to be used for subclassing only. + * In general, it is suggested to use {@link Locks#lock(Object)} + * instead. + * @param lockedObject The locked (hidden) object. The caller is + * supposed to drop all references to the locked object. + */ public Lock(final O lockedObject) { this.lockedObject = Objects.requireNonNull(lockedObject, "Locked Object"); } + /** + * Provides read (shared, non-exclusive) access to the locked (hidden) object. + * More precisely, what the method will do (in the given order): + * <ol> + * <li>Obtain a read (shared) lock on the locked (hidden) object. + * The current thread may block, until such a lock is granted. + * </li> + * <li>Invokes the given {@link FailableConsumer consumer}, passing the + * locked object as the parameter.</li> + * <li>Release the lock, as soon as the consumers invocation is done. + * If the invocation results in an error, the lock will be released + * anyways. + * </li> + * </ol> + * @param consumer The consumer, which is being invoked to use the + * hidden object, which will be passed as the consumers parameter. + * @see #acceptWriteLocked(FailableConsumer) + * @see #applyReadLocked(FailableFunction) + */ public void acceptReadLocked(final FailableConsumer<O, ?> consumer) { lockAcceptUnlock(() -> lock.readLock(), consumer); } + /** + * Provides write (exclusive) access to the locked (hidden) object. + * More precisely, what the method will do (in the given order): + * <ol> + * <li>Obtain a write (shared) lock on the locked (hidden) object. + * The current thread may block, until such a lock is granted. + * </li> + * <li>Invokes the given {@link FailableConsumer consumer}, passing the + * locked object as the parameter.</li> + * <li>Release the lock, as soon as the consumers invocation is done. + * If the invocation results in an error, the lock will be released + * anyways. + * </li> + * </ol> + * @param consumer The consumer, which is being invoked to use the + * hidden object, which will be passed as the consumers parameter. + * @see #acceptReadLocked(FailableConsumer) + * @see #applyWriteLocked(FailableFunction) + */ public void acceptWriteLocked(final FailableConsumer<O, ?> consumer) { lockAcceptUnlock(() -> lock.writeLock(), consumer); } + /** + * Provides read (shared, non-exclusive) access to the locked (hidden) + * object for the purpose of computing a result object. + * More precisely, what the method will do (in the given order): + * <ol> + * <li>Obtain a read (shared) lock on the locked (hidden) object. + * The current thread may block, until such a lock is granted. + * </li> + * <li>Invokes the given {@link FailableFunction function}, passing the + * locked object as the parameter, receiving the functions result.</li> + * <li>Release the lock, as soon as the consumers invocation is done. + * If the invocation results in an error, the lock will be released + * anyways. + * </li> + * <li> + * Return the result object, that has been received from the + * functions invocation.</li> + * </ol> + * + * <em>Example:</em> Suggest, that the hidden object is a list, and we + * wish to know the current size of the list. This might be achieved + * with the following: + * + * <pre> + * private Lock<List<Object>> listLock; + * + * public int getCurrentListSize() { + * final Integer sizeInteger + * = listLock.applyReadLocked((list) -> Integer.valueOf(list.size)); + * return sizeInteger.intValue(); + * } + * </pre> + * @param <T> The result type (both the functions, and this method's.) + * @param function The function, which is being invoked to compute the + * result. The function will receive the hidden object. + * @return The result object, which has been returned by the + * functions invocation. + * @throws IllegalStateException The result object would be, in fact, + * the hidden object. This would extend access to the hidden object + * beyond this methods lifetime and will therefore be prevented. + * @see #acceptReadLocked(FailableConsumer) + * @see #applyWriteLocked(FailableFunction) + */ public <T> T applyReadLocked(final FailableFunction<O, T, ?> function) { return lockApplyUnlock(() -> lock.readLock(), function); } + /** + * Provides write (exclusive) access to the locked (hidden) + * object for the purpose of computing a result object. + * More precisely, what the method will do (in the given order): + * <ol> + * <li>Obtain a read (shared) lock on the locked (hidden) object. + * The current thread may block, until such a lock is granted. + * </li> + * <li>Invokes the given {@link FailableFunction function}, passing the + * locked object as the parameter, receiving the functions result.</li> + * <li>Release the lock, as soon as the consumers invocation is done. + * If the invocation results in an error, the lock will be released + * anyways. + * </li> + * <li> + * Return the result object, that has been received from the + * functions invocation.</li> + * </ol> + * @param <T> The result type (both the functions, and this method's.) + * @param function The function, which is being invoked to compute the + * result. The function will receive the hidden object. + * @return The result object, which has been returned by the + * functions invocation. + * @throws IllegalStateException The result object would be, in fact, + * the hidden object. This would extend access to the hidden object + * beyond this methods lifetime and will therefore be prevented. + * @see #acceptReadLocked(FailableConsumer) + * @see #applyWriteLocked(FailableFunction) + */ public <T> T applyWriteLocked(final FailableFunction<O, T, ?> function) { return lockApplyUnlock(() -> lock.writeLock(), function); } + /** + * This method provides the actual implementation for + * {@link #acceptReadLocked(FailableConsumer)}, and + * {@link #acceptWriteLocked(FailableConsumer)}. + * @param stampSupplier A supplier for the lock. (This provides, in + * fact, a long, because a {@link StampedLock} is used internally.) + * @param consumer The consumer, which is to be given access to the + * locked (hidden) object, which will be passed as a parameter. + * @see #acceptReadLocked(FailableConsumer) + * @see #acceptWriteLocked(FailableConsumer) + * @see #lockApplyUnlock(LongSupplier, FailableFunction) + */ protected void lockAcceptUnlock(final LongSupplier stampSupplier, final FailableConsumer<O, ?> consumer) { final long stamp = stampSupplier.getAsLong(); try { @@ -106,10 +249,33 @@ public class Locks { } } + /** + * This method provides the actual implementation for + * {@link #applyReadLocked(FailableFunction)}, and + * {@link #applyWriteLocked(FailableFunction)}. + * @param <T> The result type (both the functions, and this method's.) + * @param stampSupplier A supplier for the lock. (This provides, in + * fact, a long, because a {@link StampedLock} is used internally.) + * @param function The function, which is being invoked to compute + * the result object. This function will receive the locked (hidden) + * object as a parameter. + * @return The result object, which has been returned by the + * functions invocation. + * @throws IllegalStateException The result object would be, in fact, + * the hidden object. This would extend access to the hidden object + * beyond this methods lifetime and will therefore be prevented. + * @see #applyReadLocked(FailableFunction) + * @see #applyWriteLocked(FailableFunction) + * @see #lockAcceptUnlock(LongSupplier, FailableConsumer) + */ protected <T> T lockApplyUnlock(final LongSupplier stampSupplier, final FailableFunction<O, T, ?> function) { final long stamp = stampSupplier.getAsLong(); try { - return function.apply(lockedObject); + final T t = function.apply(lockedObject); + if (t == lockedObject) { + throw new IllegalStateException("The returned object is, in fact, the hidden object."); + } + return t; } catch (final Throwable t) { throw Failable.rethrow(t); } finally { @@ -118,6 +284,14 @@ public class Locks { } } + /** + * Creates a new instance of {@link Lock} with the given locked + * (hidden) object. + * @param <O> The locked objects type. + * @param object The locked (hidden) object. + * @return The created instance, a {@link Lock lock} for the + * given object. + */ public static <O extends Object> Locks.Lock<O> lock(final O object) { return new Locks.Lock<>(object); } diff --git a/src/test/java/org/apache/commons/lang3/concurrent/LocksTest.java b/src/test/java/org/apache/commons/lang3/concurrent/LocksTest.java index 2d577ec..706281e 100644 --- a/src/test/java/org/apache/commons/lang3/concurrent/LocksTest.java +++ b/src/test/java/org/apache/commons/lang3/concurrent/LocksTest.java @@ -16,7 +16,11 @@ */ package org.apache.commons.lang3.concurrent; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import java.util.function.LongConsumer; @@ -36,6 +40,7 @@ public class LocksTest { runTest(DELAY, false, l -> assertTrue(l < NUMBER_OF_THREADS*DELAY)); } + @Test public void testWriteLock() throws Exception { final long DELAY = 100; /** If our threads are running concurrently, then we expect to be no faster @@ -44,6 +49,38 @@ public class LocksTest { runTest(DELAY, true, l -> assertTrue(l >= NUMBER_OF_THREADS*DELAY)); } + @Test + public void testResultValidation() { + final Object hidden = new Object(); + final Lock<Object> lock = Locks.lock(hidden); + final Object o1 = lock.applyReadLocked((h) -> { + return new Object(); + }); + assertNotNull(o1); + assertNotSame(hidden, o1); + final Object o2 = lock.applyWriteLocked((h) -> { + return new Object(); + }); + assertNotNull(o2); + assertNotSame(hidden, o2); + try { + lock.applyReadLocked((h) -> { + return hidden; + }); + fail("Expected Exception"); + } catch (IllegalStateException e) { + assertEquals("The returned object is, in fact, the hidden object.", e.getMessage()); + } + try { + lock.applyReadLocked((h) -> { + return hidden; + }); + fail("Expected Exception"); + } catch (IllegalStateException e) { + assertEquals("The returned object is, in fact, the hidden object.", e.getMessage()); + } + } + private void runTest(final long delay, final boolean exclusiveLock, final LongConsumer runTimeCheck) throws InterruptedException { final boolean[] booleanValues = new boolean[10]; final Lock<boolean[]> lock = Locks.lock(booleanValues);