From a2bfe30c46fda1e4282513b6e75a77db3e324670 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Mon, 22 Jun 2020 12:52:24 -0700 Subject: [PATCH] Add StampedLockFactory and ReadWriteLockFactory This adds two new factories that generate `StampedLock` and `ReadWriteLock` instances for applications that require more granular control over locking. The unit tests duplicate the verifications in `XMutexFactoryImplTest` and also introduce verifications specific to the synchronisation mechanisms. Addresses: #9 --- .../antkorwin/xsync/ReadWriteLockFactory.java | 111 ++++++ .../antkorwin/xsync/StampedLockFactory.java | 85 +++++ .../xsync/ReadWriteLockFactoryTest.java | 334 +++++++++++++++++ .../xsync/StampedLockFactoryTest.java | 337 ++++++++++++++++++ 4 files changed, 867 insertions(+) create mode 100644 src/main/java/com/antkorwin/xsync/ReadWriteLockFactory.java create mode 100644 src/main/java/com/antkorwin/xsync/StampedLockFactory.java create mode 100644 src/test/java/com/antkorwin/xsync/ReadWriteLockFactoryTest.java create mode 100644 src/test/java/com/antkorwin/xsync/StampedLockFactoryTest.java diff --git a/src/main/java/com/antkorwin/xsync/ReadWriteLockFactory.java b/src/main/java/com/antkorwin/xsync/ReadWriteLockFactory.java new file mode 100644 index 0000000..ebb55ef --- /dev/null +++ b/src/main/java/com/antkorwin/xsync/ReadWriteLockFactory.java @@ -0,0 +1,111 @@ +package com.antkorwin.xsync; + +import java.util.Objects; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.StampedLock; +import java.util.function.Supplier; + +import org.hibernate.validator.internal.util.ConcurrentReferenceHashMap; + +/** + * Created on 22.06.2020. + *

+ * The factory of {@link ReadWriteLock ReadWriteLocks}, based on + * {@link ConcurrentReferenceHashMap}. Use this if you need to allow + * multiple concurrent readers but only one writer. You can also control + * whether or not to support reentrancy and whether or not the lock should + * be fair. Depending on your usage patterns, you may opt to back the + * locks with a {@link ReentrantReadWriteLock} (default) or a + * {@link StampedLock}. Note, for a stamped lock, the optimistic locking + * model and lock type modification are not supported. + *

+ * + * @author Carlos Macasaet + */ +public class ReadWriteLockFactory { + + private static final int DEFAULT_INITIAL_CAPACITY = 16; + private static final float DEFAULT_LOAD_FACTOR = 0.75f; + private static final int DEFAULT_CONCURRENCY_LEVEL = 16; + private static final ConcurrentReferenceHashMap.ReferenceType DEFAULT_REFERENCE_TYPE = + ConcurrentReferenceHashMap.ReferenceType.WEAK; + + private final ConcurrentMap map; + private final Supplier lockSupplier; + + /** + * Create a lock factory with default settings + */ + public ReadWriteLockFactory() { + this(DEFAULT_CONCURRENCY_LEVEL, DEFAULT_REFERENCE_TYPE); + } + + /** + * Create a lock factory with default settings and a custom lock generator. + * + * @param lockSupplier a method for creating new {@link ReadWriteLock} instances. + */ + public ReadWriteLockFactory(final Supplier lockSupplier) { + this(DEFAULT_CONCURRENCY_LEVEL, DEFAULT_REFERENCE_TYPE, lockSupplier); + } + + /** + * Create a lock factory with custom settings + * + * @param concurrencyLevel the expected number of threads + * that will concurrently write to the map + * @param referenceType the reference type used for entries (soft or weak) + */ + public ReadWriteLockFactory(int concurrencyLevel, ConcurrentReferenceHashMap.ReferenceType referenceType) { + this(concurrencyLevel, referenceType, ReentrantReadWriteLock::new); + } + + /** + * Create a lock factory with custom settings + * + * @param concurrencyLevel the expected number of threads + * that will concurrently write to the map + * @param referenceType the reference type used for entries (soft or weak) + * @param lockSupplier a method for creating ReadWriteLock instances + */ + public ReadWriteLockFactory(final int concurrencyLevel, final ConcurrentReferenceHashMap.ReferenceType referenceType, + final Supplier lockSupplier) { + this(new ConcurrentReferenceHashMap<>(DEFAULT_INITIAL_CAPACITY, + DEFAULT_LOAD_FACTOR, + concurrencyLevel, + referenceType, + referenceType, + null), + lockSupplier); + } + + protected ReadWriteLockFactory(final ConcurrentMap map, + final Supplier lockSupplier) { + Objects.requireNonNull(map, "map must be provided"); + Objects.requireNonNull(lockSupplier, "lockSupplier must be provided"); + this.map = map; + this.lockSupplier = lockSupplier; + } + + /** + * Creates and returns a lock by the key. + * If the lock for this key already exists in the weak-map, + * then returns the same reference of the lock. + * + * @param key object which used as a key for synchronization + * @return lock instance created for this key + */ + public ReadWriteLock getReadWriteLock(KeyT key) { + return this.map.computeIfAbsent(key, k -> lockSupplier.get()); + } + + /** + * @return count of locks in this factory. + */ + public long size() { + return this.map.size(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/antkorwin/xsync/StampedLockFactory.java b/src/main/java/com/antkorwin/xsync/StampedLockFactory.java new file mode 100644 index 0000000..2424286 --- /dev/null +++ b/src/main/java/com/antkorwin/xsync/StampedLockFactory.java @@ -0,0 +1,85 @@ +package com.antkorwin.xsync; + + +import java.util.Objects; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.StampedLock; + +import org.hibernate.validator.internal.util.ConcurrentReferenceHashMap; + + +/** + * Created on 22.06.2020. + *

+ * The factory of locks, based on {@link ConcurrentReferenceHashMap}. + * Use this if you require the performance characteristics of + * {@link StampedLock StampedLocks} over other synchronization + * mechanisms. + *

+ * + * @author Carlos Macasaet + */ +public class StampedLockFactory { + + private static final int DEFAULT_INITIAL_CAPACITY = 16; + private static final float DEFAULT_LOAD_FACTOR = 0.75f; + private static final int DEFAULT_CONCURRENCY_LEVEL = 16; + private static final ConcurrentReferenceHashMap.ReferenceType DEFAULT_REFERENCE_TYPE = + ConcurrentReferenceHashMap.ReferenceType.WEAK; + + private final ConcurrentMap map; + + /** + * Create a factory with default settings + */ + public StampedLockFactory() { + this(new ConcurrentReferenceHashMap<>(DEFAULT_INITIAL_CAPACITY, + DEFAULT_LOAD_FACTOR, + DEFAULT_CONCURRENCY_LEVEL, + DEFAULT_REFERENCE_TYPE, + DEFAULT_REFERENCE_TYPE, + null)); + } + + /** + * Creating a factory with custom settings + * + * @param concurrencyLevel the expected number of threads + * that will concurrently write to the map + * @param referenceType the reference type used for entries (soft or weak) + */ + public StampedLockFactory(int concurrencyLevel, + ConcurrentReferenceHashMap.ReferenceType referenceType) { + this(new ConcurrentReferenceHashMap<>(DEFAULT_INITIAL_CAPACITY, + DEFAULT_LOAD_FACTOR, + concurrencyLevel, + referenceType, + referenceType, + null)); + } + + protected StampedLockFactory(final ConcurrentMap map) { + Objects.requireNonNull(map, "map must be provided"); + this.map = map; + } + + /** + * Creates and returns a lock by the key. If the lock for this key + * already exists(or use by another thread), then returns the same + * reference of the lock. + * + * @param key object which used as a key for synchronization + * @return lock instance created for this key + */ + public StampedLock getLock(KeyT key) { + return this.map.computeIfAbsent(key, k -> new StampedLock()); + } + + /** + * @return count of locks in this factory. + */ + public long size() { + return this.map.size(); + } + +} \ No newline at end of file diff --git a/src/test/java/com/antkorwin/xsync/ReadWriteLockFactoryTest.java b/src/test/java/com/antkorwin/xsync/ReadWriteLockFactoryTest.java new file mode 100644 index 0000000..aef7af2 --- /dev/null +++ b/src/test/java/com/antkorwin/xsync/ReadWriteLockFactoryTest.java @@ -0,0 +1,334 @@ +package com.antkorwin.xsync; + +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.equalTo; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.locks.StampedLock; +import java.util.function.Supplier; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.hibernate.validator.internal.util.ConcurrentReferenceHashMap; +import org.junit.Test; + +import com.antkorwin.commonutils.concurrent.ConcurrentSet; +import com.antkorwin.commonutils.gc.GcUtils; + +/** + * Created on 22.06.2020. + * + * @author Carlos Macasaet + */ +public class ReadWriteLockFactoryTest { + + private static final int TIMEOUT_FOR_PREVENTION_OF_DEADLOCK = 30000; + private static final int NUMBER_OF_LOCKS = 100_000; + private static final int NUMBER_OF_ITERATIONS = NUMBER_OF_LOCKS * 100; + private static final String ID_STRING = "c117c526-606e-41b6-8197-1a6ba779f69b"; + + @Test + public void testGetSameLockFromTwoDifferentInstanceOfEqualsKeys() { + // Arrange + ReadWriteLockFactory factory = new ReadWriteLockFactory<>(); + UUID firstId = UUID.fromString(ID_STRING); + UUID secondId = UUID.fromString(ID_STRING); + // Check precondition + assertThat(firstId).isNotSameAs(secondId); + assertThat(firstId).isEqualTo(secondId); + + // Act + ReadWriteLock firstLock = factory.getReadWriteLock(firstId); + ReadWriteLock secondLock = factory.getReadWriteLock(secondId); + + // Asserts + assertThat(firstLock).isNotNull(); + assertThat(secondLock).isNotNull(); + assertThat(firstLock).isEqualTo(secondLock); + assertThat(firstLock).isSameAs(secondLock); + } + + @Test + public void testWithRunGCAfterReleaseFirstLock() throws InterruptedException { + // Arrange + ConcurrentReferenceHashMap map = new ConcurrentReferenceHashMap<>(16, 0.75f, 16, + ConcurrentReferenceHashMap.ReferenceType.WEAK, ConcurrentReferenceHashMap.ReferenceType.WEAK, null); + ReadWriteLockFactory factory = new ReadWriteLockFactory<>(map, ReentrantReadWriteLock::new); + UUID firstId = UUID.fromString(ID_STRING); + UUID secondId = UUID.fromString(ID_STRING); + + // Act + ReadWriteLock firstLock = factory.getReadWriteLock(firstId); + int firstHashCode = System.identityHashCode(firstLock); + firstLock = null; + + GcUtils.tryToAllocateAllAvailableMemory(); + GcUtils.fullFinalization(); + map.purgeStaleEntries(); + + await().atMost(5, TimeUnit.SECONDS) + .until(factory::size, equalTo(0L)); + // Now, the weak-map of lock factory is empty, + // because all of the lock references released + assertThat(factory.size()).isEqualTo(0); + + ReadWriteLock secondLock = factory.getReadWriteLock(secondId); + int secondHashCode = System.identityHashCode(secondLock); + + // Asserts + assertThat(factory.size()).isEqualTo(1L); + assertThat(firstHashCode).isNotEqualTo(secondHashCode); + } + + @Test + public void testSizeOfLockFactoryMap() { + // Arrange + ReadWriteLockFactory factory = new ReadWriteLockFactory<>(); + UUID firstId = UUID.fromString(ID_STRING); + UUID secondId = UUID.fromString(ID_STRING); + UUID thirdId = UUID.randomUUID(); + Collection set = new HashSet<>(); + + // Act + set.add(factory.getReadWriteLock(firstId)); + set.add(factory.getReadWriteLock(secondId)); + set.add(factory.getReadWriteLock(thirdId)); + + // Asserts + assertThat(factory.size()).isEqualTo(2); + } + + @Test + public void testEqualityOfReturnedLocksBySystemIdentityHashCode() { + // Arrange + ReadWriteLockFactory factory = new ReadWriteLockFactory<>(); + UUID firstId = UUID.fromString(ID_STRING); + UUID secondId = UUID.fromString(ID_STRING); + UUID thirdId = UUID.fromString(ID_STRING); + + // Act + ReadWriteLock firstLock = factory.getReadWriteLock(firstId); + ReadWriteLock secondLock = factory.getReadWriteLock(secondId); + ReadWriteLock thirdLock = factory.getReadWriteLock(thirdId); + + // Assert + assertThat(System.identityHashCode(firstLock)) + .isEqualTo(System.identityHashCode(secondLock)); + + assertThat(System.identityHashCode(firstLock)) + .isEqualTo(System.identityHashCode(thirdLock)); + } + + @Test + public void testALotOfHashCodes() { + // Arrange + ReadWriteLockFactory factory = new ReadWriteLockFactory<>(); + Set setOfHash = ConcurrentSet.getInstance(); + List references = Collections.synchronizedList(new ArrayList<>()); + + // first key and lock: + UUID key = UUID.fromString(ID_STRING); + ReadWriteLock firstLock = factory.getReadWriteLock(key); + + // check that all same keys (by the value) will give + // the only one instance of lock + for (int i = 0; i < NUMBER_OF_ITERATIONS; i++) { + + UUID sameId = UUID.fromString(ID_STRING); + // and now, we save the key-reference in the list, + // because the GC can delete an unused reference: + references.add(sameId); + + // Act + ReadWriteLock lock = factory.getReadWriteLock(sameId); + setOfHash.add(System.identityHashCode(lock)); + + // Assert + assertThat(lock).isSameAs(firstLock); + } + + // Assertions + assertThat(factory.size()).isEqualTo(1); + assertThat(setOfHash.size()).isEqualTo(1); + assertThat(references).hasSize(NUMBER_OF_ITERATIONS); + assertThat(key).isNotNull(); + } + + @Test(timeout = TIMEOUT_FOR_PREVENTION_OF_DEADLOCK) + public void testConcurrency() { + // Arrange + ReadWriteLockFactory factory = new ReadWriteLockFactory<>(); + + List ids = IntStream.range(0, NUMBER_OF_LOCKS) + .boxed() + .map(i -> UUID.randomUUID()) + .collect(toList()); + + Set results = ConcurrentSet.getInstance(); + + // Act + IntStream.range(0, NUMBER_OF_ITERATIONS) + .boxed() + .parallel() + .forEach(i -> { + UUID uuid = ids.get(i % NUMBER_OF_LOCKS); + ReadWriteLock lock = factory.getReadWriteLock(uuid); + results.add(lock); + }); + + // Asserts + await().atMost(10, TimeUnit.SECONDS) + .until(results::size, equalTo(NUMBER_OF_LOCKS)); + + assertThat(results).hasSize(NUMBER_OF_LOCKS); + + await().atMost(10, TimeUnit.SECONDS) + .until(factory::size, equalTo((long) NUMBER_OF_LOCKS)); + + assertThat(factory.size()).isEqualTo(NUMBER_OF_LOCKS); + } + + @Test + public void testExceptionThrownWhenTryToGetLockWithNullKey() { + // Arrange + ReadWriteLockFactory factory = new ReadWriteLockFactory<>(); + + // Act / Asserts + assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> factory.getReadWriteLock(null)); + } + + @Test(timeout = TIMEOUT_FOR_PREVENTION_OF_DEADLOCK) + public void testWithCustomConcurrencySettingsWeakAndLevel() { + // Arrange + ReadWriteLockFactory factory = new ReadWriteLockFactory<>(8, + ConcurrentReferenceHashMap.ReferenceType.WEAK); + + List ids = IntStream.range(0, NUMBER_OF_LOCKS) + .boxed() + .map(i -> UUID.randomUUID()) + .collect(toList()); + + List results = Collections.synchronizedList(new ArrayList<>()); + + // Act + IntStream.range(0, NUMBER_OF_ITERATIONS) + .boxed() + .parallel() + .forEach(i -> { + UUID uuid = ids.get(i % NUMBER_OF_LOCKS); + ReadWriteLock lock = factory.getReadWriteLock(uuid); + results.add(lock); + }); + + // Asserts + await().atMost(10, TimeUnit.SECONDS) + .until(results::size, equalTo(NUMBER_OF_ITERATIONS)); + + Set distinctResult = results.stream() + .distinct() + .collect(toSet()); + + assertThat(distinctResult).hasSize(NUMBER_OF_LOCKS); + } + + @Test(timeout = TIMEOUT_FOR_PREVENTION_OF_DEADLOCK) + public void testSupportForReentrantImplementations() { + final Supplier unfairSupplier = ReentrantReadWriteLock::new; + final Supplier fairSupplier = () -> new ReentrantReadWriteLock(true); + Stream.of(unfairSupplier, fairSupplier).forEach(this::testSupportForSupplier); + } + + public void testSupportForSupplier(final Supplier supplier) { + // Arrange + final ReadWriteLockFactory factory = new ReadWriteLockFactory<>(supplier); + final Map cache = new ConcurrentHashMap<>(); + final UUID first = UUID.randomUUID(); + final UUID second = UUID.randomUUID(); + + // Act + Stream.of(first, second, first, second).parallel().forEach(id -> { + final ReadWriteLock lock = factory.getReadWriteLock(id); + lock.readLock().lock(); + try { + if (!cache.containsKey(id)) { + lock.readLock().unlock(); + lock.writeLock().lock(); + try { + if (!cache.containsKey(id)) { + cache.put(id, 0); + } + } finally { + lock.readLock().lock(); + lock.writeLock().unlock(); + } + } + + // Asserts + assertThat(cache.get(id)).isEqualTo(0); + } finally { + lock.readLock().unlock(); + } + }); + } + + @Test(timeout = TIMEOUT_FOR_PREVENTION_OF_DEADLOCK) + public void testSupportForNonReentrantImplementation() { + // Arrange + final ReadWriteLockFactory factory = new ReadWriteLockFactory(() -> new StampedLock().asReadWriteLock()); + final Map cache = new ConcurrentHashMap<>(); + final UUID id = UUID.fromString(ID_STRING); + final Runnable readTask = () -> { + final ReadWriteLock lock = factory.getReadWriteLock(id); + lock.readLock().lock(); + try { + cache.containsKey(id); + } finally { + lock.readLock().unlock(); + } + }; + final Runnable writeTask = () -> { + final ReadWriteLock lock = factory.getReadWriteLock(id); + lock.writeLock().lock(); + try { + final int count = cache.containsKey(id) ? cache.get(id) : 0; + cache.put(id, count + 1); + } finally { + lock.writeLock().unlock(); + } + }; + final ExecutorService executor = Executors.newFixedThreadPool(2); + + // Act + executor.execute(readTask); + executor.execute(readTask); + executor.execute(writeTask); + executor.execute(readTask); + executor.execute(writeTask); + executor.execute(readTask); + executor.execute(readTask); + executor.shutdown(); + await().atMost(1, TimeUnit.SECONDS).until(executor::isTerminated); + + // Asserts + assertThat(cache.containsKey(id)).isTrue(); + assertThat(cache.get(id)).isEqualTo(2); + } + +} \ No newline at end of file diff --git a/src/test/java/com/antkorwin/xsync/StampedLockFactoryTest.java b/src/test/java/com/antkorwin/xsync/StampedLockFactoryTest.java new file mode 100644 index 0000000..c80fa70 --- /dev/null +++ b/src/test/java/com/antkorwin/xsync/StampedLockFactoryTest.java @@ -0,0 +1,337 @@ +package com.antkorwin.xsync; + +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.equalTo; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.StampedLock; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.hibernate.validator.internal.util.ConcurrentReferenceHashMap; +import org.junit.Test; + +import com.antkorwin.commonutils.concurrent.ConcurrentSet; +import com.antkorwin.commonutils.gc.GcUtils; + +/** + * Created on 22.06.2020. + * + * @author Carlos Macasaet + */ +public class StampedLockFactoryTest { + + private static final int TIMEOUT_FOR_PREVENTION_OF_DEADLOCK = 30000; + private static final int NUMBER_OF_LOCKS = 100_000; + private static final int NUMBER_OF_ITERATIONS = NUMBER_OF_LOCKS * 100; + private static final String ID_STRING = "c117c526-606e-41b6-8197-1a6ba779f69b"; + + @Test + public void testGetSameLockFromTwoDifferentInstanceOfEqualsKeys() { + // Arrange + StampedLockFactory factory = new StampedLockFactory<>(); + UUID firstId = UUID.fromString(ID_STRING); + UUID secondId = UUID.fromString(ID_STRING); + // Check precondition + assertThat(firstId).isNotSameAs(secondId); + assertThat(firstId).isEqualTo(secondId); + + // Act + StampedLock firstLock = factory.getLock(firstId); + StampedLock secondLock = factory.getLock(secondId); + + // Asserts + assertThat(firstLock).isNotNull(); + assertThat(secondLock).isNotNull(); + assertThat(firstLock).isEqualTo(secondLock); + assertThat(firstLock).isSameAs(secondLock); + } + + @Test + public void testWithRunGCAfterReleaseFirstLock() throws InterruptedException { + // Arrange + ConcurrentReferenceHashMap map = new ConcurrentReferenceHashMap<>(16, 0.75f, 16, + ConcurrentReferenceHashMap.ReferenceType.WEAK, ConcurrentReferenceHashMap.ReferenceType.WEAK, null); + StampedLockFactory factory = new StampedLockFactory<>(map); + UUID firstId = UUID.fromString(ID_STRING); + UUID secondId = UUID.fromString(ID_STRING); + + // Act + StampedLock firstLock = factory.getLock(firstId); + int firstHashCode = System.identityHashCode(firstLock); + firstLock = null; + + GcUtils.tryToAllocateAllAvailableMemory(); + GcUtils.fullFinalization(); + map.purgeStaleEntries(); + + await().atMost(5, TimeUnit.SECONDS) + .until(factory::size, equalTo(0L)); + // Now, the weak-map of lock factory is empty, + // because all of the lock references released + assertThat(factory.size()).isEqualTo(0); + + StampedLock secondLock = factory.getLock(secondId); + int secondHashCode = System.identityHashCode(secondLock); + + // Asserts + assertThat(factory.size()).isEqualTo(1L); + assertThat(firstHashCode).isNotEqualTo(secondHashCode); + } + + @Test + public void testSizeOfFactoryMap() { + // Arrange + StampedLockFactory factory = new StampedLockFactory<>(); + UUID firstId = UUID.fromString(ID_STRING); + UUID secondId = UUID.fromString(ID_STRING); + UUID thirdId = UUID.randomUUID(); + Collection set = new HashSet<>(); + + // Act + set.add(factory.getLock(firstId)); + set.add(factory.getLock(secondId)); + set.add(factory.getLock(thirdId)); + + // Asserts + assertThat(factory.size()).isEqualTo(2); + } + + @Test + public void testEqualityOfReturnedLocksBySystemIdentityHashCode() { + // Arrange + StampedLockFactory factory = new StampedLockFactory<>(); + UUID firstId = UUID.fromString(ID_STRING); + UUID secondId = UUID.fromString(ID_STRING); + UUID thirdId = UUID.fromString(ID_STRING); + + // Act + StampedLock firstLock = factory.getLock(firstId); + StampedLock secondLock = factory.getLock(secondId); + StampedLock thirdLock = factory.getLock(thirdId); + + // Assert + assertThat(System.identityHashCode(firstLock)) + .isEqualTo(System.identityHashCode(secondLock)); + + assertThat(System.identityHashCode(firstLock)) + .isEqualTo(System.identityHashCode(thirdLock)); + } + + @Test + public void testALotOfHashCodes() { + // Arrange + StampedLockFactory factory = new StampedLockFactory<>(); + Set setOfHash = ConcurrentSet.getInstance(); + List references = Collections.synchronizedList(new ArrayList<>()); + + // first key and lock: + UUID key = UUID.fromString(ID_STRING); + StampedLock firstLock = factory.getLock(key); + + // check that all same keys (by the value) will give + // the only one instance of lock + for (int i = 0; i < NUMBER_OF_ITERATIONS; i++) { + + UUID sameId = UUID.fromString(ID_STRING); + // and now, we save the key-reference in the list, + // because the GC can delete an unused reference: + references.add(sameId); + + // Act + StampedLock lock = factory.getLock(sameId); + setOfHash.add(System.identityHashCode(lock)); + + // Assert + assertThat(lock).isSameAs(firstLock); + } + + // Assertions + assertThat(factory.size()).isEqualTo(1); + assertThat(setOfHash.size()).isEqualTo(1); + assertThat(references).hasSize(NUMBER_OF_ITERATIONS); + assertThat(key).isNotNull(); + } + + @Test(timeout = TIMEOUT_FOR_PREVENTION_OF_DEADLOCK) + public void testConcurrency() { + // Arrange + StampedLockFactory factory = new StampedLockFactory<>(); + + List ids = IntStream.range(0, NUMBER_OF_LOCKS) + .boxed() + .map(i -> UUID.randomUUID()) + .collect(toList()); + + Set results = ConcurrentSet.getInstance(); + + // Act + IntStream.range(0, NUMBER_OF_ITERATIONS) + .boxed() + .parallel() + .forEach(i -> { + UUID uuid = ids.get(i % NUMBER_OF_LOCKS); + StampedLock lock = factory.getLock(uuid); + results.add(lock); + }); + + // Asserts + await().atMost(10, TimeUnit.SECONDS) + .until(results::size, equalTo(NUMBER_OF_LOCKS)); + + assertThat(results).hasSize(NUMBER_OF_LOCKS); + + await().atMost(10, TimeUnit.SECONDS) + .until(factory::size, equalTo((long) NUMBER_OF_LOCKS)); + + assertThat(factory.size()).isEqualTo(NUMBER_OF_LOCKS); + } + + @Test + public void testExceptionThrownWhenTryToGetLockWithNullKey() { + // Arrange + StampedLockFactory factory = new StampedLockFactory<>(); + + // Act / Assert + assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> factory.getLock(null)); + } + + + @Test(timeout = TIMEOUT_FOR_PREVENTION_OF_DEADLOCK) + public void testWithCustomConcurrencySettingsWeakAndLevel() { + // Arrange + StampedLockFactory factory = + new StampedLockFactory<>(8, ConcurrentReferenceHashMap.ReferenceType.WEAK); + + List ids = IntStream.range(0, NUMBER_OF_LOCKS) + .boxed() + .map(i -> UUID.randomUUID()) + .collect(toList()); + + List results = Collections.synchronizedList(new ArrayList<>()); + + // Act + IntStream.range(0, NUMBER_OF_ITERATIONS) + .boxed() + .parallel() + .forEach(i -> { + UUID uuid = ids.get(i % NUMBER_OF_LOCKS); + StampedLock lock = factory.getLock(uuid); + results.add(lock); + }); + + // Asserts + await().atMost(10, TimeUnit.SECONDS) + .until(results::size, equalTo(NUMBER_OF_ITERATIONS)); + + Set distinctResult = results.stream() + .distinct() + .collect(toSet()); + + assertThat(distinctResult).hasSize(NUMBER_OF_LOCKS); + } + + @Test(timeout = TIMEOUT_FOR_PREVENTION_OF_DEADLOCK) + public void testSupportForStampedLock() { + // Arrange + final StampedLockFactory factory = new StampedLockFactory(); + final Map cache = new ConcurrentHashMap<>(); + final UUID first = UUID.randomUUID(); + final UUID second = UUID.randomUUID(); + + // Act + Stream.of(first, second, first, second).parallel().forEach(id -> { + final StampedLock lock = factory.getLock(id); + long stamp = lock.readLock(); + try { + while (!cache.containsKey(id)) { + final long writeStamp = lock.tryConvertToWriteLock(stamp); + if (writeStamp != 0) { + cache.put(id, 0); + final long readStamp = lock.tryConvertToReadLock(writeStamp); + if (readStamp != 0) { + stamp = readStamp; + } else { + lock.unlockWrite(writeStamp); + stamp = lock.readLock(); + } + } else { + lock.unlockRead(stamp); + stamp = lock.writeLock(); + } + } + + // Asserts + assertThat(cache.get(id)).isEqualTo(0); + } finally { + lock.unlock(stamp); + } + }); + } + + @Test(timeout = TIMEOUT_FOR_PREVENTION_OF_DEADLOCK) + public void testSupportForOptimisticLock() { + // Arrange + final StampedLockFactory factory = new StampedLockFactory(); + final Map cache = new ConcurrentHashMap<>(); + final UUID id = UUID.fromString(ID_STRING); + final Runnable readTask = () -> { + final StampedLock lock = factory.getLock(id); + long stamp = lock.tryOptimisticRead(); + cache.containsKey(id); + cache.get(id); + if (lock.validate(stamp)) { + return; + } + stamp = lock.readLock(); + try { + cache.containsKey(id); + cache.get(id); + } finally { + lock.unlockRead(stamp); + } + }; + final Runnable writeTask = () -> { + final StampedLock lock = factory.getLock(id); + long stamp = lock.writeLock(); + try { + final int count = cache.containsKey(id) ? cache.get(id) : 0; + cache.put(id, count + 1); + } finally { + lock.unlockWrite(stamp); + } + }; + final ExecutorService executor = Executors.newFixedThreadPool(2); + + // Act + executor.execute(readTask); + executor.execute(readTask); + executor.execute(writeTask); + executor.execute(readTask); + executor.execute(writeTask); + executor.execute(readTask); + executor.execute(readTask); + executor.shutdown(); + await().atMost(1, TimeUnit.SECONDS).until(executor::isTerminated); + + // Asserts + assertThat(cache.containsKey(id)).isTrue(); + assertThat(cache.get(id)).isEqualTo(2); + } + +} \ No newline at end of file